From 10004ea2f4372070db34b3464baee29323625c8f Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sun, 22 Dec 2024 20:50:42 -0500 Subject: [PATCH 1/8] refactor: split Staff Dev role into separate roles, add CM role --- app/Filament/Pages/AdminTools.php | 7 +- app/Filament/Pages/MostReportedGames.php | 8 +- app/Filament/Resources/UserResource.php | 137 ++++++++++++++++++ .../Resources/UserResource/Pages/Roles.php | 35 +++-- app/Models/Role.php | 25 +++- app/Policies/AchievementAuthorPolicy.php | 4 - app/Policies/AchievementPolicy.php | 3 - app/Policies/AchievementSetAuthorPolicy.php | 5 - app/Policies/AchievementSetClaimPolicy.php | 2 - app/Policies/AchievementSetPolicy.php | 6 - app/Policies/CommentPolicy.php | 13 -- app/Policies/GameAchievementSetPolicy.php | 5 - app/Policies/GameHashPolicy.php | 4 - app/Policies/GamePolicy.php | 18 +-- app/Policies/GameSetPolicy.php | 2 - app/Policies/LeaderboardEntryPolicy.php | 3 +- app/Policies/LeaderboardPolicy.php | 6 - app/Policies/MemoryNotePolicy.php | 4 - app/Policies/MessagePolicy.php | 1 - app/Policies/NewsPolicy.php | 3 - app/Policies/TicketPolicy.php | 2 - app/Policies/TriggerTicketPolicy.php | 1 - app/Policies/UserPolicy.php | 94 +++++++++++- app/Providers/AuthServiceProvider.php | 9 +- config/roles.php | 30 +++- lang/en/permission.php | 5 +- .../js/common/utils/generatedAppConstants.ts | 5 +- resources/js/types/generated.d.ts | 5 +- 28 files changed, 326 insertions(+), 116 deletions(-) diff --git a/app/Filament/Pages/AdminTools.php b/app/Filament/Pages/AdminTools.php index 335b97c935..0f8e00e3af 100644 --- a/app/Filament/Pages/AdminTools.php +++ b/app/Filament/Pages/AdminTools.php @@ -7,6 +7,7 @@ use App\Models\Role; use App\Models\User; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; class AdminTools extends Page { @@ -23,7 +24,11 @@ class AdminTools extends Page public static function canAccess(): bool { /** @var User $user */ - $user = auth()->user(); + $user = Auth::user(); + + if (!$user) { + return false; + } return $user->hasAnyRole([Role::MODERATOR, Role::ADMINISTRATOR]); } diff --git a/app/Filament/Pages/MostReportedGames.php b/app/Filament/Pages/MostReportedGames.php index 52383b1723..2647dc791f 100644 --- a/app/Filament/Pages/MostReportedGames.php +++ b/app/Filament/Pages/MostReportedGames.php @@ -7,6 +7,7 @@ use App\Models\Role; use App\Models\User; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; class MostReportedGames extends Page { @@ -21,13 +22,16 @@ class MostReportedGames extends Page public static function canAccess(): bool { /** @var User $user */ - $user = auth()->user(); + $user = Auth::user(); + + if (!$user) { + return false; + } return $user->hasAnyRole([ Role::ADMINISTRATOR, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, - Role::DEVELOPER_STAFF, ]); } } diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 1f70e0fb6e..e28e8caffe 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -14,6 +14,7 @@ use Filament\Forms\Form; use Filament\Infolists; use Filament\Infolists\Infolist; +use Filament\Notifications\Notification; use Filament\Pages\Page; use Filament\Tables; use Filament\Tables\Filters; @@ -22,6 +23,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletingScope; +use Illuminate\Support\Facades\Auth; class UserResource extends Resource { @@ -60,6 +62,9 @@ public static function getGloballySearchableAttributes(): array public static function infolist(Infolist $infolist): Infolist { + /** @var User $user */ + $user = Auth::user(); + return $infolist ->columns(1) ->schema([ @@ -72,6 +77,7 @@ public static function infolist(Infolist $infolist): Infolist Infolists\Components\ImageEntry::make('avatar_url') ->label('Avatar') ->size(config('media.icon.lg.width')), + Infolists\Components\TextEntry::make('Motto'), ]), Infolists\Components\Group::make() @@ -81,6 +87,7 @@ public static function infolist(Infolist $infolist): Infolist ->formatStateUsing(fn (string $state): string => __('permission.role.' . $state)) ->color(fn (string $state): string => Role::toFilamentColor($state)) ->hidden(fn ($record) => $record->roles->isEmpty()), + Infolists\Components\TextEntry::make('Permissions') ->label('Permissions (legacy)') ->badge() @@ -93,6 +100,104 @@ public static function infolist(Infolist $infolist): Infolist Permissions::Moderator => 'warning', default => 'gray', }), + + Infolists\Components\Actions::make([ + Infolists\Components\Actions\Action::make('promoteToJuniorDev') + ->label('Promote to Junior Developer') + ->icon('fas-user-plus') + ->requiresConfirmation() + ->modalDescription("Are you absolutely sure? If this isn't a direction promotion to full developer, the user must have an approved set plan and have read the Developer Code of Conduct. This action will be logged and attached to your name.") + ->action(function (User $targetUser) { + $targetUser->assignRole(Role::DEVELOPER_JUNIOR); + $targetUser->setAttribute('Permissions', Permissions::JuniorDeveloper); + $targetUser->save(); + + Notification::make() + ->success() + ->body('User has been promoted to Junior Developer.') + ->send(); + }) + ->visible(function (User $targetUser) use ($user): bool { + if ($targetUser->hasAnyRole([Role::DEVELOPER_JUNIOR, Role::DEVELOPER])) { + return false; + } + + return $user->can('issueDeveloperPromotions', $targetUser); + }), + + Infolists\Components\Actions\Action::make('promoteToFullDev') + ->label('Promote to Full Developer') + ->icon('fas-user-plus') + ->requiresConfirmation() + ->modalDescription('Are you absolutely sure? This will give the user full developer powers on the site. This action will be logged and attached to your name.') + ->action(function (User $targetUser) { + $targetUser->removeRole(Role::DEVELOPER_JUNIOR); + $targetUser->assignRole(Role::DEVELOPER); + $targetUser->setAttribute('Permissions', Permissions::Developer); + $targetUser->save(); + + Notification::make() + ->success() + ->body('User has been promoted to Developer.') + ->send(); + }) + ->visible(function (User $targetUser) use ($user): bool { + // Need to be a JrDev before being promoted to Dev. + // Even for dev reinstatement, just press the promote button twice. + if ( + !$targetUser->hasRole(Role::DEVELOPER_JUNIOR) + || $targetUser->hasRole(Role::DEVELOPER) + ) { + return false; + } + + return $user->can('issueDeveloperPromotions', $targetUser); + }), + + Infolists\Components\Actions\Action::make('demoteFromAllDevRoles') + ->label('Demote from All Developer Roles') + ->icon('fas-user-minus') + ->requiresConfirmation() + ->modalDescription('Are you absolutely sure? This is a destructive action that will drop all the user\'s claims. This action will be logged and attached to your name.') + ->color('danger') + ->action(function (User $targetUser) { + // If we don't explicitly check for each role, the role removals + // will be recorded to the Audit Log, even if the user doesn't have + // that particular role. + if ($targetUser->hasRole(Role::DEVELOPER_JUNIOR)) { + $targetUser->removeRole(Role::DEVELOPER_JUNIOR); + } + if ($targetUser->hasRole(Role::DEVELOPER)) { + $targetUser->removeRole(Role::DEVELOPER); + } + if ($targetUser->hasRole(Role::DEV_COMPLIANCE)) { + $targetUser->removeRole(Role::DEV_COMPLIANCE); + } + if ($targetUser->hasRole(Role::QUALITY_ASSURANCE)) { + $targetUser->removeRole(Role::QUALITY_ASSURANCE); + } + if ($targetUser->hasRole(Role::CODE_REVIEWER)) { + $targetUser->removeRole(Role::CODE_REVIEWER); + } + + $currentPermissions = (int) $targetUser->getAttribute('Permissions'); + if ($currentPermissions > Permissions::Registered && $currentPermissions < Permissions::Moderator) { + $targetUser->setAttribute('Permissions', Permissions::Registered); + } + + $targetUser->save(); + + Notification::make() + ->success() + ->body('User has been demoted from all developer roles.') + ->send(); + }) + ->visible(function (User $targetUser) use ($user): bool { + return + $user->can('issueJuniorDeveloperDemotions', $targetUser) + || $user->can('issueFullDeveloperDemotions', $targetUser); + }), + ]), ]), Infolists\Components\Group::make() ->schema([ @@ -111,22 +216,27 @@ public static function infolist(Infolist $infolist): Infolist ->schema([ Infolists\Components\TextEntry::make('id') ->label('ID'), + Infolists\Components\TextEntry::make('Created') ->label('Joined') ->dateTime(), + Infolists\Components\TextEntry::make('LastLogin') ->label('Last login at') ->dateTime(), + Infolists\Components\TextEntry::make('DeleteRequested') ->label('Deleted requested at') ->dateTime() ->hidden(fn ($state) => !$state) ->color('warning'), + Infolists\Components\TextEntry::make('Deleted') ->label('Deleted at') ->dateTime() ->hidden(fn ($state) => !$state) ->color('danger'), + Infolists\Components\IconEntry::make('Untracked') ->label('Ranked') ->boolean() @@ -134,9 +244,11 @@ public static function infolist(Infolist $infolist): Infolist ->trueIcon('heroicon-o-x-circle') ->falseColor('success') ->falseIcon('heroicon-o-check-circle'), + Infolists\Components\IconEntry::make('ManuallyVerified') ->label('Forum verified') ->boolean(), + Infolists\Components\TextEntry::make('muted_until') ->hidden(function ($state) { if (!$state) { @@ -165,6 +277,7 @@ public static function form(Form $form): Form Forms\Components\TextInput::make('Motto') ->maxLength(50), ]), + Forms\Components\Section::make() ->grow(false) ->schema([ @@ -185,8 +298,10 @@ public static function form(Form $form): Form $component->state($formattedDate); } }), + Forms\Components\Toggle::make('ManuallyVerified') ->label('Forum verified'), + Forms\Components\Toggle::make('Untracked'), ]), ])->from('md'), @@ -200,25 +315,31 @@ public static function table(Table $table): Table Tables\Columns\ImageColumn::make('avatar_url') ->label('') ->size(config('media.icon.sm.width')), + Tables\Columns\TextColumn::make('ID') ->label('ID') ->searchable() ->sortable(), + Tables\Columns\TextColumn::make('User') ->description(fn (User $record): string => $record->display_name) ->label('Username') ->searchable(), + Tables\Columns\TextColumn::make('display_name') ->searchable() ->toggleable(isToggledHiddenByDefault: true), + // Tables\Columns\TextColumn::make('email_verified_at') // ->dateTime() // ->sortable() // ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('roles.name') ->badge() ->formatStateUsing(fn (string $state): string => __('permission.role.' . $state)) ->color(fn (string $state): string => Role::toFilamentColor($state)), + Tables\Columns\TextColumn::make('Permissions') ->label('Legacy permissions') ->badge() @@ -231,17 +352,21 @@ public static function table(Table $table): Table Permissions::Moderator => 'warning', default => 'gray', }), + // Tables\Columns\TextColumn::make('country'), // Tables\Columns\TextColumn::make('timezone'), // Tables\Columns\TextColumn::make('locale'), + Tables\Columns\IconColumn::make('ManuallyVerified') ->label('Forum verified') ->boolean() ->alignCenter(), + // Tables\Columns\TextColumn::make('forum_verified_at') // ->dateTime() // ->sortable() // ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\IconColumn::make('Untracked') ->label('Ranked') ->boolean() @@ -250,38 +375,47 @@ public static function table(Table $table): Table ->falseColor('success') ->falseIcon('heroicon-o-check-circle') ->alignCenter(), + // Tables\Columns\TextColumn::make('unranked_at') // ->dateTime() // ->sortable(), + // Tables\Columns\TextColumn::make('banned_at') // ->dateTime() // ->sortable(), + // Tables\Columns\TextColumn::make('muted_until') // ->dateTime() // ->sortable(), + Tables\Columns\IconColumn::make('UserWallActive') ->label('Wall active') ->boolean() ->alignCenter(), + Tables\Columns\TextColumn::make('Created') ->label('Created at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('LastLogin') ->label('Last login at') ->dateTime() ->sortable(), + Tables\Columns\TextColumn::make('Updated') ->label('Updated at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('DeleteRequested') ->label('Deleted requested at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('Deleted') ->label('Deleted at') ->dateTime() @@ -296,6 +430,7 @@ public static function table(Table $table): Table collect(Permissions::cases()) ->mapWithKeys(fn ($value) => [$value => __(Permissions::toString($value))]) ), + Filters\TrashedFilter::make(), ]) ->deferFilters() @@ -305,9 +440,11 @@ public static function table(Table $table): Table Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), ])->dropdown(false), + Tables\Actions\Action::make('roles') ->url(fn ($record) => UserResource::getUrl('roles', ['record' => $record])) ->icon('fas-lock'), + Tables\Actions\Action::make('audit-log') ->url(fn ($record) => UserResource::getUrl('audit-log', ['record' => $record])) ->icon('fas-clock-rotate-left'), diff --git a/app/Filament/Resources/UserResource/Pages/Roles.php b/app/Filament/Resources/UserResource/Pages/Roles.php index a6eb3d2424..e625963722 100644 --- a/app/Filament/Resources/UserResource/Pages/Roles.php +++ b/app/Filament/Resources/UserResource/Pages/Roles.php @@ -16,6 +16,13 @@ use Illuminate\Support\Facades\Auth; use Spatie\Permission\Models\Role as SpatieRole; +/** + * For JrDev / Dev promotions and demotions, generally those actions + * will occur on the UserResource infolist. + * + * Privileged users can promote/demote from this relation manager, and + * have elevated access when doing so. + */ class Roles extends ManageRelatedRecords { protected static string $resource = UserResource::class; @@ -55,26 +62,19 @@ public function table(Table $table): Table if ($attachedRole->name === Role::DEVELOPER_JUNIOR) { $targetUser->removeRole(Role::DEVELOPER); - $targetUser->removeRole(Role::DEVELOPER_STAFF); $targetUser->removeRole(Role::DEVELOPER_RETIRED); + $this->removeDeveloperStaffRoles($targetUser); // jr devs cannot be staff $newPermissions = Permissions::JuniorDeveloper; } elseif ($attachedRole->name === Role::DEVELOPER) { $targetUser->removeRole(Role::DEVELOPER_JUNIOR); - $targetUser->removeRole(Role::DEVELOPER_STAFF); - $targetUser->removeRole(Role::DEVELOPER_RETIRED); - - $newPermissions = Permissions::Developer; - } elseif ($attachedRole->name === Role::DEVELOPER_STAFF) { - $targetUser->removeRole(Role::DEVELOPER_JUNIOR); - $targetUser->removeRole(Role::DEVELOPER); $targetUser->removeRole(Role::DEVELOPER_RETIRED); $newPermissions = Permissions::Developer; } elseif ($attachedRole->name === Role::DEVELOPER_RETIRED) { $targetUser->removeRole(Role::DEVELOPER_JUNIOR); $targetUser->removeRole(Role::DEVELOPER); - $targetUser->removeRole(Role::DEVELOPER_STAFF); + $this->removeDeveloperStaffRoles($targetUser); // retired devs cannot be staff $newPermissions = Permissions::Registered; } else { @@ -104,12 +104,14 @@ public function table(Table $table): Table if (!in_array($record->name, [ Role::DEVELOPER_JUNIOR, Role::DEVELOPER, - Role::DEVELOPER_STAFF, Role::DEVELOPER_RETIRED, ])) { return; } + // When manually detaching any dev role, remove all staff dev roles. + $this->removeDeveloperStaffRoles($targetUser); + // Keep legacy permissions in sync. $currentPermissions = (int) $targetUser->getAttribute('Permissions'); // Don't strip moderation power away if the user already has it. @@ -123,4 +125,17 @@ public function table(Table $table): Table Tables\Actions\BulkActionGroup::make([]), ]); } + + private function removeDeveloperStaffRoles(User $targetUser): void + { + if ($targetUser->hasRole(Role::QUALITY_ASSURANCE)) { + $targetUser->removeRole(Role::QUALITY_ASSURANCE); + } + if ($targetUser->hasRole(Role::DEV_COMPLIANCE)) { + $targetUser->removeRole(Role::DEV_COMPLIANCE); + } + if ($targetUser->hasRole(Role::CODE_REVIEWER)) { + $targetUser->removeRole(Role::CODE_REVIEWER); + } + } } diff --git a/app/Models/Role.php b/app/Models/Role.php index 78ec44c565..65e928a2f2 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -5,10 +5,12 @@ namespace App\Models; use Fico7489\Laravel\Pivot\Traits\PivotEventTrait; +use Illuminate\Support\Facades\Auth; +use Spatie\Permission\Models\Role as SpatieRole; use Spatie\TypeScriptTransformer\Attributes\TypeScript; #[TypeScript('UserRole')] -class Role extends \Spatie\Permission\Models\Role +class Role extends SpatieRole { /* * Providers Traits @@ -29,7 +31,11 @@ class Role extends \Spatie\Permission\Models\Role public const GAME_HASH_MANAGER = 'game-hash-manager'; - public const DEVELOPER_STAFF = 'developer-staff'; // staff + public const DEV_COMPLIANCE = 'dev-compliance'; + + public const QUALITY_ASSURANCE = 'quality-assurance'; + + public const CODE_REVIEWER = 'code-reviewer'; public const DEVELOPER = 'developer'; @@ -75,6 +81,8 @@ class Role extends \Spatie\Permission\Models\Role // vanity roles assigned by admin + public const COMMUNITY_MANAGER = 'community-manager'; // effectively a moderator + public const DEVELOPER_RETIRED = 'developer-retired'; public static function toFilamentColor(string $role): string @@ -90,7 +98,9 @@ public static function toFilamentColor(string $role): string // creator roles assigned by admin Role::GAME_HASH_MANAGER => 'warning', - Role::DEVELOPER_STAFF => 'success', + Role::DEV_COMPLIANCE => 'success', + Role::QUALITY_ASSURANCE => 'success', + Role::CODE_REVIEWER => 'success', Role::DEVELOPER => 'success', Role::DEVELOPER_JUNIOR => 'success', Role::ARTIST => 'success', @@ -117,7 +127,9 @@ public static function toFilamentColor(string $role): string // vanity roles assigned by admin + Role::COMMUNITY_MANAGER => 'info', // effectively a moderator Role::DEVELOPER_RETIRED => 'primary', + default => 'gray', }; } @@ -128,11 +140,14 @@ public static function boot() // record users role attach/detach in audit log + /** @var User $user */ + $user = Auth::user(); + static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { if ($relationName === 'users') { foreach ($pivotIds as $pivotId) { $user = User::find($pivotId); - activity()->causedBy(auth()->user())->performedOn($user) + activity()->causedBy($user)->performedOn($user) ->withProperty('relationships', ['roles' => [$model->id]]) ->withProperty('attributes', ['roles' => [$model->id => []]]) ->event('pivotAttached') @@ -145,7 +160,7 @@ public static function boot() if ($relationName === 'users') { foreach ($pivotIds as $pivotId) { $user = User::find($pivotId); - activity()->causedBy(auth()->user())->performedOn($user) + activity()->causedBy($user)->performedOn($user) ->withProperty('relationships', ['roles' => [$model->id]]) ->event('pivotDetached') ->log('pivotDetached'); diff --git a/app/Policies/AchievementAuthorPolicy.php b/app/Policies/AchievementAuthorPolicy.php index 449b19b4c4..d46d0eeb44 100644 --- a/app/Policies/AchievementAuthorPolicy.php +++ b/app/Policies/AchievementAuthorPolicy.php @@ -17,7 +17,6 @@ class AchievementAuthorPolicy public function manage(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::MODERATOR, Role::TEAM_ACCOUNT, @@ -53,7 +52,6 @@ public function update(User $user, AchievementAuthor $achievementAuthor): bool public function delete(User $user, AchievementAuthor $achievementAuthor): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::MODERATOR, Role::TEAM_ACCOUNT, ]); @@ -62,7 +60,6 @@ public function delete(User $user, AchievementAuthor $achievementAuthor): bool public function restore(User $user, AchievementAuthor $achievementAuthor): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::MODERATOR, Role::TEAM_ACCOUNT, ]); @@ -77,7 +74,6 @@ public function canUpsertTask(User $user, AchievementAuthorTask $task): bool { // These roles can assign any type of credit. $alwaysAllowed = [ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::MODERATOR, Role::TEAM_ACCOUNT, diff --git a/app/Policies/AchievementPolicy.php b/app/Policies/AchievementPolicy.php index 4b3c74fbed..82990ab579 100644 --- a/app/Policies/AchievementPolicy.php +++ b/app/Policies/AchievementPolicy.php @@ -21,7 +21,6 @@ public function manage(User $user): bool /* * developers may at least upload new achievements to the server, create code notes, etc */ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, @@ -76,7 +75,6 @@ public function update(User $user, Achievement $achievement): bool /* * developers may at least upload new achievements to the server, create code notes, etc */ - Role::DEVELOPER_STAFF, Role::DEVELOPER, /* @@ -119,7 +117,6 @@ public function updateField(User $user, ?Achievement $achievement, string $field $roleFieldPermissions = [ Role::DEVELOPER_JUNIOR => ['Title', 'Description', 'type', 'Points', 'DisplayOrder'], Role::DEVELOPER => ['Title', 'Description', 'Flags', 'type', 'Points', 'DisplayOrder'], - Role::DEVELOPER_STAFF => ['Title', 'Description', 'Flags', 'type', 'Points', 'DisplayOrder'], Role::WRITER => ['Title', 'Description'], ]; diff --git a/app/Policies/AchievementSetAuthorPolicy.php b/app/Policies/AchievementSetAuthorPolicy.php index 78e1ecbf0e..88da518e87 100644 --- a/app/Policies/AchievementSetAuthorPolicy.php +++ b/app/Policies/AchievementSetAuthorPolicy.php @@ -16,7 +16,6 @@ class AchievementSetAuthorPolicy public function manage(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, Role::ARTIST, @@ -36,7 +35,6 @@ public function view(?User $user, AchievementSetAuthor $achievementSetAuthor): b public function create(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::ARTIST, ]); @@ -45,7 +43,6 @@ public function create(User $user): bool public function update(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::ARTIST, ]); @@ -54,7 +51,6 @@ public function update(User $user): bool public function delete(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::ARTIST, ]); @@ -63,7 +59,6 @@ public function delete(User $user): bool public function restore(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::ARTIST, ]); diff --git a/app/Policies/AchievementSetClaimPolicy.php b/app/Policies/AchievementSetClaimPolicy.php index fbce15ca2f..065c3632a9 100644 --- a/app/Policies/AchievementSetClaimPolicy.php +++ b/app/Policies/AchievementSetClaimPolicy.php @@ -16,7 +16,6 @@ class AchievementSetClaimPolicy public function manage(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, ]); @@ -35,7 +34,6 @@ public function view(?User $user, AchievementSetClaim $achievementSetClaim): boo public function create(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, ]); diff --git a/app/Policies/AchievementSetPolicy.php b/app/Policies/AchievementSetPolicy.php index bbf0b9f9b1..085d500596 100644 --- a/app/Policies/AchievementSetPolicy.php +++ b/app/Policies/AchievementSetPolicy.php @@ -17,7 +17,6 @@ public function manage(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, // Juniors can see set management, but can't manipulate sets. Role::DEVELOPER_JUNIOR, @@ -38,7 +37,6 @@ public function create(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, ]); @@ -48,7 +46,6 @@ public function update(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -57,7 +54,6 @@ public function delete(User $user, ?AchievementSet $achievementSet = null): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -66,7 +62,6 @@ public function restore(User $user, AchievementSet $achievementSet): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -80,7 +75,6 @@ public function markGameHashAsIncompatible(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } diff --git a/app/Policies/CommentPolicy.php b/app/Policies/CommentPolicy.php index 1a0f8a9962..0c99d10e4c 100644 --- a/app/Policies/CommentPolicy.php +++ b/app/Policies/CommentPolicy.php @@ -47,19 +47,6 @@ public function viewAny(?User $user, Model $commentable): bool public function create(?User $user, ?Model $commentable = null, ?int $articleType = null): bool { if ($user?->isMuted()) { - // Even when muted, developers may still comment on tickets for their own achievements. - // TODO this is silly. delete all of this. - if ($commentable !== null && $commentable instanceof \App\Models\Ticket) { - $commentable->loadMissing(['achievement.developer']); - - $didAuthorAchievement = $commentable->achievement->developer->id === $user->id; - - return - $didAuthorAchievement - && $commentable->is_open - && $user->hasAnyRole([Role::DEVELOPER_STAFF, Role::DEVELOPER]); - } - return false; } diff --git a/app/Policies/GameAchievementSetPolicy.php b/app/Policies/GameAchievementSetPolicy.php index 810ddef8a8..1f998e506b 100644 --- a/app/Policies/GameAchievementSetPolicy.php +++ b/app/Policies/GameAchievementSetPolicy.php @@ -17,7 +17,6 @@ public function manage(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, // Juniors can see set management, but can't manipulate sets. Role::DEVELOPER_JUNIOR, @@ -40,7 +39,6 @@ public function create(User $user): bool // they are not included in this list. return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -49,7 +47,6 @@ public function update(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -58,7 +55,6 @@ public function delete(User $user, ?GameAchievementSet $gameAchievementSet = nul { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -67,7 +63,6 @@ public function restore(User $user, GameAchievementSet $gameAchievementSet): boo { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } diff --git a/app/Policies/GameHashPolicy.php b/app/Policies/GameHashPolicy.php index fd80f1e776..994f585cda 100644 --- a/app/Policies/GameHashPolicy.php +++ b/app/Policies/GameHashPolicy.php @@ -17,7 +17,6 @@ public function manage(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -36,7 +35,6 @@ public function create(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -45,7 +43,6 @@ public function update(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -64,7 +61,6 @@ public function forceDelete(User $user, GameHash $gameHash): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } diff --git a/app/Policies/GamePolicy.php b/app/Policies/GamePolicy.php index 3acfa14263..be5655476f 100644 --- a/app/Policies/GamePolicy.php +++ b/app/Policies/GamePolicy.php @@ -18,7 +18,6 @@ public function manage(User $user): bool return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, @@ -33,9 +32,7 @@ public function viewAny(?User $user): bool public function view(?User $user, Game $game): bool { - /* - * TODO: check age gate - */ + // Age gates are handled at the UI level. return true; } @@ -44,7 +41,6 @@ public function create(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - // Role::DEVELOPER_STAFF, // Role::DEVELOPER, ]); } @@ -53,7 +49,6 @@ public function update(User $user, Game $game): bool { $canAlwaysUpdate = $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); @@ -98,7 +93,6 @@ public function updateField(User $user, Game $game, string $fieldName): bool Role::DEVELOPER_JUNIOR => ['GuideURL', 'Developer', 'Publisher', 'Genre', 'released_at', 'released_at_granularity'], Role::DEVELOPER => ['Title', 'GuideURL', 'Developer', 'Publisher', 'Genre', 'released_at', 'released_at_granularity'], - Role::DEVELOPER_STAFF => ['Title', 'sort_title', 'GuideURL', 'Developer', 'Publisher', 'Genre', 'released_at', 'released_at_granularity'], ]; $userRoles = $user->getRoleNames(); @@ -129,28 +123,20 @@ public function createForumTopic(User $user, Game $game): bool } return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::FORUM_MANAGER, Role::MODERATOR, ]); } - // TODO rename to viewActivitylog or use manage() ? public function viewModifications(User $user): bool { - return $user->hasAnyRole([ - Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, - Role::DEVELOPER, - Role::DEVELOPER_JUNIOR, - ]); + return $this->manage($user); } public function manageContributionCredit(User $user, Game $game): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::ARTIST, ]); diff --git a/app/Policies/GameSetPolicy.php b/app/Policies/GameSetPolicy.php index 46e58c0e90..68309ada32 100644 --- a/app/Policies/GameSetPolicy.php +++ b/app/Policies/GameSetPolicy.php @@ -19,7 +19,6 @@ public function manage(User $user): bool Role::GAME_HASH_MANAGER, Role::GAME_EDITOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -43,7 +42,6 @@ public function create(User $user): bool // TODO enable after dropping GameAlternatives // return $user->hasAnyRole([ // Role::ADMINISTRATOR, - // Role::DEVELOPER_STAFF, // Role::GAME_EDITOR, // ]); } diff --git a/app/Policies/LeaderboardEntryPolicy.php b/app/Policies/LeaderboardEntryPolicy.php index 2d5d09c856..5b37f31a06 100644 --- a/app/Policies/LeaderboardEntryPolicy.php +++ b/app/Policies/LeaderboardEntryPolicy.php @@ -17,7 +17,6 @@ public function manage(User $user): bool { return $user->hasAnyRole([ Role::CHEAT_INVESTIGATOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, ]); @@ -50,7 +49,7 @@ public function delete(User $user, LeaderboardEntry $leaderboardEntry): bool Role::ROOT, Role::ADMINISTRATOR, Role::MODERATOR, - Role::DEVELOPER_STAFF, + Role::DEVELOPER, ]; diff --git a/app/Policies/LeaderboardPolicy.php b/app/Policies/LeaderboardPolicy.php index a095bfa01e..70548371b1 100644 --- a/app/Policies/LeaderboardPolicy.php +++ b/app/Policies/LeaderboardPolicy.php @@ -17,7 +17,6 @@ class LeaderboardPolicy public function manage(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, ]); @@ -40,7 +39,6 @@ public function create(User $user, ?Game $game = null): bool } return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -52,7 +50,6 @@ public function update(User $user, Leaderboard $leaderboard): bool } return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -60,7 +57,6 @@ public function update(User $user, Leaderboard $leaderboard): bool public function delete(User $user, Leaderboard $leaderboard): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -73,7 +69,6 @@ public function restore(User $user, Leaderboard $leaderboard): bool public function forceDelete(User $user, Leaderboard $leaderboard): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -81,7 +76,6 @@ public function forceDelete(User $user, Leaderboard $leaderboard): bool public function resetAllEntries(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } diff --git a/app/Policies/MemoryNotePolicy.php b/app/Policies/MemoryNotePolicy.php index a7fd40ca84..15ac5c79e0 100644 --- a/app/Policies/MemoryNotePolicy.php +++ b/app/Policies/MemoryNotePolicy.php @@ -17,7 +17,6 @@ public function manage(User $user): bool { return $user->hasAnyRole([ Role::DEVELOPER_JUNIOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -36,7 +35,6 @@ public function create(User $user): bool { return $user->hasAnyRole([ Role::DEVELOPER_JUNIOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -49,7 +47,6 @@ public function update(User $user, MemoryNote $memoryNote): bool } return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -62,7 +59,6 @@ public function delete(User $user, MemoryNote $memoryNote): bool } return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } diff --git a/app/Policies/MessagePolicy.php b/app/Policies/MessagePolicy.php index f0cdbac169..be4898b60c 100644 --- a/app/Policies/MessagePolicy.php +++ b/app/Policies/MessagePolicy.php @@ -69,7 +69,6 @@ public function sendToRecipient(User $user, User $targetUser): bool $canUserAlwaysPierceNoContactPreference = $user->hasAnyRole([ Role::ADMINISTRATOR, Role::DEVELOPER_JUNIOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::EVENT_MANAGER, Role::FORUM_MANAGER, diff --git a/app/Policies/NewsPolicy.php b/app/Policies/NewsPolicy.php index 24da36acd3..3f1327b9df 100644 --- a/app/Policies/NewsPolicy.php +++ b/app/Policies/NewsPolicy.php @@ -17,7 +17,6 @@ public function manage(User $user): bool { return $user->hasAnyRole([ Role::ADMINISTRATOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::EVENT_MANAGER, Role::MODERATOR, @@ -40,7 +39,6 @@ public function create(User $user): bool { return $user->hasAnyRole([ Role::ADMINISTRATOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::EVENT_MANAGER, Role::MODERATOR, @@ -53,7 +51,6 @@ public function update(User $user, News $news): bool { return $user->hasAnyRole([ Role::ADMINISTRATOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::EVENT_MANAGER, Role::MODERATOR, diff --git a/app/Policies/TicketPolicy.php b/app/Policies/TicketPolicy.php index e2642e9be6..7caef317a1 100644 --- a/app/Policies/TicketPolicy.php +++ b/app/Policies/TicketPolicy.php @@ -18,7 +18,6 @@ public function manage(User $user): bool return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, Role::TICKET_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, ]); @@ -46,7 +45,6 @@ public function create(User $user): bool public function updateState(User $user): bool { return $user->hasAnyRole([ - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::TICKET_MANAGER, ]); diff --git a/app/Policies/TriggerTicketPolicy.php b/app/Policies/TriggerTicketPolicy.php index 87321d2e67..d157a14d42 100644 --- a/app/Policies/TriggerTicketPolicy.php +++ b/app/Policies/TriggerTicketPolicy.php @@ -19,7 +19,6 @@ public function manage(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, Role::DEVELOPER_JUNIOR, ]); diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index 1b48b22f02..3a073524e8 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -16,7 +16,19 @@ class UserPolicy public function manage(User $user): bool { - return $this->requireAdministrativePrivileges($user); + return $user->hasAnyRole([ + // admins + Role::ROOT, + Role::ADMINISTRATOR, + + // moderation + Role::MODERATOR, + + // staff developers + Role::CODE_REVIEWER, + Role::DEV_COMPLIANCE, + Role::QUALITY_ASSURANCE, + ]); } public function viewAny(?User $user): bool @@ -251,6 +263,86 @@ public function clearUserWall(User $user, User $model): bool return $this->requireAdministrativePrivileges($user, $model); } + public function issueDeveloperPromotions(User $user, User $model): bool + { + // users cannot promote themselves + if ($user->is($model)) { + return false; + } + + // moderated users cannot be promoted + if ($model->isBanned() || $model->isMuted()) { + return false; + } + + $canAlwaysPromote = [ + Role::ROOT, + Role::ADMINISTRATOR, + Role::MODERATOR, + + Role::DEV_COMPLIANCE, + ]; + if ($user->hasAnyRole($canAlwaysPromote)) { + return true; + } + + // Code reviewers can promote standard users to Junior Developers. + if ($user->hasRole(Role::CODE_REVIEWER) && !$model->hasRole(Role::DEVELOPER_JUNIOR)) { + return true; + } + } + + public function issueJuniorDeveloperDemotions(User $user, User $model): bool + { + // self-demotion is an awkward UX, just disallow it for now. + if ($user->is($model)) { + return false; + } + + // If the target user isn't already a JrDev, return false. + if (!$model->hasRole(Role::DEVELOPER_JUNIOR)) { + return false; + } + + return $user->hasAnyRole([ + Role::ROOT, + Role::ADMINISTRATOR, + Role::MODERATOR, + + Role::DEV_COMPLIANCE, + Role::CODE_REVIEWER, + ]); + } + + public function issueFullDeveloperDemotions(User $user, User $model): bool + { + // self-demotion is an awkward UX, just disallow it for now. + if ($user->is($model)) { + return false; + } + + // You'll need to actually detach the role for these target users, + // and that requires elevated privileges in order to access the + // Roles relation manager. This is for safety. + if ($model->hasAnyRole([Role::ADMINISTRATOR, Role::MODERATOR, Role::DEV_COMPLIANCE])) { + return false; + } + + // If the target user doesn't have any demotable roles, then just return false. + $demotableDevRoles = [Role::DEVELOPER, Role::DEVELOPER_JUNIOR]; + if (!$model->hasAnyRole($demotableDevRoles)) { + return false; + } + + return $user->hasAnyRole([ + Role::ROOT, + Role::ADMINISTRATOR, + Role::MODERATOR, + + Role::DEV_COMPLIANCE, + ]); + } + private function requireAdministrativePrivileges(User $user, ?User $model = null): bool { if (!$model) { diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 9021c3edd3..317883d63f 100755 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -33,7 +33,6 @@ public function boot(): void Role::ROOT, Role::ADMINISTRATOR, Role::MODERATOR, - // Role::COMMUNITY_MANAGER, // rather a mix of moderator and specialized management role? Role::EVENT_MANAGER, Role::FORUM_MANAGER, Role::GAME_HASH_MANAGER, @@ -44,9 +43,7 @@ public function boot(): void Role::ARTIST, Role::WRITER, Role::GAME_EDITOR, - ]) - // TODO remove as soon as permission matrix is in place - || $user->getAttribute('Permissions') >= Permissions::JuniorDeveloper); + ])); /* * can "create". meant for creator tools opt-in @@ -55,9 +52,7 @@ public function boot(): void Role::DEVELOPER, Role::ARTIST, Role::WRITER, - ]) - // TODO remove as soon as permission matrix is in place - || $user->getAttribute('Permissions') >= Permissions::JuniorDeveloper); + ])); /* * settings diff --git a/config/roles.php b/config/roles.php index df30347a01..8137508e81 100644 --- a/config/roles.php +++ b/config/roles.php @@ -16,8 +16,10 @@ $adminAssignable = [ Role::ARTIST, Role::CHEAT_INVESTIGATOR, + Role::CODE_REVIEWER, + Role::COMMUNITY_MANAGER, + Role::DEV_COMPLIANCE, Role::DEVELOPER_JUNIOR, - Role::DEVELOPER_STAFF, Role::DEVELOPER_RETIRED, Role::DEVELOPER, Role::EVENT_MANAGER, @@ -27,15 +29,11 @@ Role::MODERATOR, Role::NEWS_MANAGER, Role::PLAY_TESTER, + Role::QUALITY_ASSURANCE, Role::TICKET_MANAGER, Role::WRITER, ]; -$staffDevAssignable = [ - Role::DEVELOPER, - Role::DEVELOPER_JUNIOR, -]; - /* * Note: permissions are not assigned to roles in database for now - check AuthServiceProvider */ @@ -73,9 +71,20 @@ 'legacy_role' => Permissions::Moderator, ], [ - 'name' => Role::DEVELOPER_STAFF, // staff dev + 'name' => Role::DEV_COMPLIANCE, + 'display' => 4, + 'staff' => true, + 'legacy_role' => Permissions::Developer, + ], + [ + 'name' => Role::QUALITY_ASSURANCE, + 'display' => 4, + 'staff' => true, + 'legacy_role' => Permissions::Developer, + ], + [ + 'name' => Role::CODE_REVIEWER, 'display' => 4, - 'assign' => $staffDevAssignable, 'staff' => true, 'legacy_role' => Permissions::Developer, ], @@ -182,6 +191,11 @@ // vanity roles assigned by admin + [ + 'name' => Role::COMMUNITY_MANAGER, + 'display' => 3, + 'legacy_role' => Permissions::Registered, + ], [ 'name' => Role::DEVELOPER_RETIRED, 'display' => 5, diff --git a/lang/en/permission.php b/lang/en/permission.php index 6d5d44d128..c53da952b8 100755 --- a/lang/en/permission.php +++ b/lang/en/permission.php @@ -15,7 +15,9 @@ // creator roles - Role::DEVELOPER_STAFF => __('Staff Developer'), + Role::DEV_COMPLIANCE => __('Developer Compliance'), + Role::QUALITY_ASSURANCE => __('Quality Assurance'), + Role::CODE_REVIEWER => __('Code Reviewer'), Role::DEVELOPER => __('Developer'), Role::DEVELOPER_JUNIOR => __('Junior Developer'), Role::ARTIST => __('Artist'), @@ -41,6 +43,7 @@ // vanity roles assigned by admins + Role::COMMUNITY_MANAGER => __('Community Manager'), Role::DEVELOPER_RETIRED => __('Developer (retired)'), ], ]; diff --git a/resources/js/common/utils/generatedAppConstants.ts b/resources/js/common/utils/generatedAppConstants.ts index 349838d4c5..91b516c339 100644 --- a/resources/js/common/utils/generatedAppConstants.ts +++ b/resources/js/common/utils/generatedAppConstants.ts @@ -63,7 +63,9 @@ export const UserRole = { ADMINISTRATOR: 'administrator', RELEASE_MANAGER: 'release-manager', GAME_HASH_MANAGER: 'game-hash-manager', - DEVELOPER_STAFF: 'developer-staff', + DEV_COMPLIANCE: 'dev-compliance', + QUALITY_ASSURANCE: 'quality-assurance', + CODE_REVIEWER: 'code-reviewer', DEVELOPER: 'developer', DEVELOPER_JUNIOR: 'developer-junior', ARTIST: 'artist', @@ -81,6 +83,7 @@ export const UserRole = { ENGINEER: 'engineer', TEAM_ACCOUNT: 'team-account', BETA: 'beta', + COMMUNITY_MANAGER: 'community-manager', DEVELOPER_RETIRED: 'developer-retired', } as const; diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index 3074d01a9e..cf8558407b 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -268,7 +268,9 @@ declare namespace App.Models { | 'administrator' | 'release-manager' | 'game-hash-manager' - | 'developer-staff' + | 'dev-compliance' + | 'quality-assurance' + | 'code-reviewer' | 'developer' | 'developer-junior' | 'artist' @@ -286,6 +288,7 @@ declare namespace App.Models { | 'engineer' | 'team-account' | 'beta' + | 'community-manager' | 'developer-retired'; } declare namespace App.Platform.Data { From eca3996d3776571606b29dd6a0f2f607f44fbcdb Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sun, 22 Dec 2024 20:51:43 -0500 Subject: [PATCH 2/8] chore: phpstan --- app/Policies/UserPolicy.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index 3a073524e8..f4c42224b4 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -290,6 +290,8 @@ public function issueDeveloperPromotions(User $user, User $model): bool if ($user->hasRole(Role::CODE_REVIEWER) && !$model->hasRole(Role::DEVELOPER_JUNIOR)) { return true; } + + return false; } public function issueJuniorDeveloperDemotions(User $user, User $model): bool From df6a710a6bcf1d3b8a84ceb0e69f566c23b7e0bd Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sun, 22 Dec 2024 20:57:50 -0500 Subject: [PATCH 3/8] chore: simplify --- app/Filament/Resources/UserResource.php | 98 ------------------- .../Resources/UserResource/Pages/Roles.php | 7 -- app/Policies/UserPolicy.php | 82 ---------------- public/request/user/update.php | 6 ++ 4 files changed, 6 insertions(+), 187 deletions(-) diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index e28e8caffe..32aebbd649 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -100,104 +100,6 @@ public static function infolist(Infolist $infolist): Infolist Permissions::Moderator => 'warning', default => 'gray', }), - - Infolists\Components\Actions::make([ - Infolists\Components\Actions\Action::make('promoteToJuniorDev') - ->label('Promote to Junior Developer') - ->icon('fas-user-plus') - ->requiresConfirmation() - ->modalDescription("Are you absolutely sure? If this isn't a direction promotion to full developer, the user must have an approved set plan and have read the Developer Code of Conduct. This action will be logged and attached to your name.") - ->action(function (User $targetUser) { - $targetUser->assignRole(Role::DEVELOPER_JUNIOR); - $targetUser->setAttribute('Permissions', Permissions::JuniorDeveloper); - $targetUser->save(); - - Notification::make() - ->success() - ->body('User has been promoted to Junior Developer.') - ->send(); - }) - ->visible(function (User $targetUser) use ($user): bool { - if ($targetUser->hasAnyRole([Role::DEVELOPER_JUNIOR, Role::DEVELOPER])) { - return false; - } - - return $user->can('issueDeveloperPromotions', $targetUser); - }), - - Infolists\Components\Actions\Action::make('promoteToFullDev') - ->label('Promote to Full Developer') - ->icon('fas-user-plus') - ->requiresConfirmation() - ->modalDescription('Are you absolutely sure? This will give the user full developer powers on the site. This action will be logged and attached to your name.') - ->action(function (User $targetUser) { - $targetUser->removeRole(Role::DEVELOPER_JUNIOR); - $targetUser->assignRole(Role::DEVELOPER); - $targetUser->setAttribute('Permissions', Permissions::Developer); - $targetUser->save(); - - Notification::make() - ->success() - ->body('User has been promoted to Developer.') - ->send(); - }) - ->visible(function (User $targetUser) use ($user): bool { - // Need to be a JrDev before being promoted to Dev. - // Even for dev reinstatement, just press the promote button twice. - if ( - !$targetUser->hasRole(Role::DEVELOPER_JUNIOR) - || $targetUser->hasRole(Role::DEVELOPER) - ) { - return false; - } - - return $user->can('issueDeveloperPromotions', $targetUser); - }), - - Infolists\Components\Actions\Action::make('demoteFromAllDevRoles') - ->label('Demote from All Developer Roles') - ->icon('fas-user-minus') - ->requiresConfirmation() - ->modalDescription('Are you absolutely sure? This is a destructive action that will drop all the user\'s claims. This action will be logged and attached to your name.') - ->color('danger') - ->action(function (User $targetUser) { - // If we don't explicitly check for each role, the role removals - // will be recorded to the Audit Log, even if the user doesn't have - // that particular role. - if ($targetUser->hasRole(Role::DEVELOPER_JUNIOR)) { - $targetUser->removeRole(Role::DEVELOPER_JUNIOR); - } - if ($targetUser->hasRole(Role::DEVELOPER)) { - $targetUser->removeRole(Role::DEVELOPER); - } - if ($targetUser->hasRole(Role::DEV_COMPLIANCE)) { - $targetUser->removeRole(Role::DEV_COMPLIANCE); - } - if ($targetUser->hasRole(Role::QUALITY_ASSURANCE)) { - $targetUser->removeRole(Role::QUALITY_ASSURANCE); - } - if ($targetUser->hasRole(Role::CODE_REVIEWER)) { - $targetUser->removeRole(Role::CODE_REVIEWER); - } - - $currentPermissions = (int) $targetUser->getAttribute('Permissions'); - if ($currentPermissions > Permissions::Registered && $currentPermissions < Permissions::Moderator) { - $targetUser->setAttribute('Permissions', Permissions::Registered); - } - - $targetUser->save(); - - Notification::make() - ->success() - ->body('User has been demoted from all developer roles.') - ->send(); - }) - ->visible(function (User $targetUser) use ($user): bool { - return - $user->can('issueJuniorDeveloperDemotions', $targetUser) - || $user->can('issueFullDeveloperDemotions', $targetUser); - }), - ]), ]), Infolists\Components\Group::make() ->schema([ diff --git a/app/Filament/Resources/UserResource/Pages/Roles.php b/app/Filament/Resources/UserResource/Pages/Roles.php index e625963722..89ec8c5337 100644 --- a/app/Filament/Resources/UserResource/Pages/Roles.php +++ b/app/Filament/Resources/UserResource/Pages/Roles.php @@ -16,13 +16,6 @@ use Illuminate\Support\Facades\Auth; use Spatie\Permission\Models\Role as SpatieRole; -/** - * For JrDev / Dev promotions and demotions, generally those actions - * will occur on the UserResource infolist. - * - * Privileged users can promote/demote from this relation manager, and - * have elevated access when doing so. - */ class Roles extends ManageRelatedRecords { protected static string $resource = UserResource::class; diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index f4c42224b4..76881414a8 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -263,88 +263,6 @@ public function clearUserWall(User $user, User $model): bool return $this->requireAdministrativePrivileges($user, $model); } - public function issueDeveloperPromotions(User $user, User $model): bool - { - // users cannot promote themselves - if ($user->is($model)) { - return false; - } - - // moderated users cannot be promoted - if ($model->isBanned() || $model->isMuted()) { - return false; - } - - $canAlwaysPromote = [ - Role::ROOT, - Role::ADMINISTRATOR, - Role::MODERATOR, - - Role::DEV_COMPLIANCE, - ]; - if ($user->hasAnyRole($canAlwaysPromote)) { - return true; - } - - // Code reviewers can promote standard users to Junior Developers. - if ($user->hasRole(Role::CODE_REVIEWER) && !$model->hasRole(Role::DEVELOPER_JUNIOR)) { - return true; - } - - return false; - } - - public function issueJuniorDeveloperDemotions(User $user, User $model): bool - { - // self-demotion is an awkward UX, just disallow it for now. - if ($user->is($model)) { - return false; - } - - // If the target user isn't already a JrDev, return false. - if (!$model->hasRole(Role::DEVELOPER_JUNIOR)) { - return false; - } - - return $user->hasAnyRole([ - Role::ROOT, - Role::ADMINISTRATOR, - Role::MODERATOR, - - Role::DEV_COMPLIANCE, - Role::CODE_REVIEWER, - ]); - } - - public function issueFullDeveloperDemotions(User $user, User $model): bool - { - // self-demotion is an awkward UX, just disallow it for now. - if ($user->is($model)) { - return false; - } - - // You'll need to actually detach the role for these target users, - // and that requires elevated privileges in order to access the - // Roles relation manager. This is for safety. - if ($model->hasAnyRole([Role::ADMINISTRATOR, Role::MODERATOR, Role::DEV_COMPLIANCE])) { - return false; - } - - // If the target user doesn't have any demotable roles, then just return false. - $demotableDevRoles = [Role::DEVELOPER, Role::DEVELOPER_JUNIOR]; - if (!$model->hasAnyRole($demotableDevRoles)) { - return false; - } - - return $user->hasAnyRole([ - Role::ROOT, - Role::ADMINISTRATOR, - Role::MODERATOR, - - Role::DEV_COMPLIANCE, - ]); - } - private function requireAdministrativePrivileges(User $user, ?User $model = null): bool { if (!$model) { diff --git a/public/request/user/update.php b/public/request/user/update.php index 8d6ec539d2..9247462619 100644 --- a/public/request/user/update.php +++ b/public/request/user/update.php @@ -41,9 +41,15 @@ } elseif ($value === Permissions::Registered) { $foundTargetUser->removeRole(Role::DEVELOPER_JUNIOR); $foundTargetUser->removeRole(Role::DEVELOPER); + $foundTargetUser->removeRole(Role::DEV_COMPLIANCE); + $foundTargetUser->removeRole(Role::QUALITY_ASSURANCE); + $foundTargetUser->removeRole(Role::CODE_REVIEWER); $foundTargetUser->removeRole(Role::MODERATOR); } elseif ($value === Permissions::JuniorDeveloper) { $foundTargetUser->removeRole(Role::DEVELOPER); + $foundTargetUser->removeRole(Role::DEV_COMPLIANCE); + $foundTargetUser->removeRole(Role::QUALITY_ASSURANCE); + $foundTargetUser->removeRole(Role::CODE_REVIEWER); $foundTargetUser->removeRole(Role::MODERATOR); $foundTargetUser->assignRole(Role::DEVELOPER_JUNIOR); From 637b6b88fcae7692c5cdb67f4aac5517f5e53503 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sun, 22 Dec 2024 21:08:57 -0500 Subject: [PATCH 4/8] chore: pint --- app/Filament/Resources/UserResource.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 32aebbd649..c9b2714f1b 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -14,7 +14,6 @@ use Filament\Forms\Form; use Filament\Infolists; use Filament\Infolists\Infolist; -use Filament\Notifications\Notification; use Filament\Pages\Page; use Filament\Tables; use Filament\Tables\Filters; From aed7dac72cc029a6f88907b3ad6fd8af3ef9f392 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Mon, 30 Dec 2024 14:21:22 -0500 Subject: [PATCH 5/8] fix: set staff dev role assignment restrictions --- .../Resources/UserResource/Pages/Roles.php | 16 +++++++++++++++- app/Models/Role.php | 14 ++++++++++---- app/Policies/UserPolicy.php | 14 +------------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/Filament/Resources/UserResource/Pages/Roles.php b/app/Filament/Resources/UserResource/Pages/Roles.php index 89ec8c5337..d49c132f72 100644 --- a/app/Filament/Resources/UserResource/Pages/Roles.php +++ b/app/Filament/Resources/UserResource/Pages/Roles.php @@ -46,7 +46,21 @@ public function table(Table $table): Table ->authorize(fn () => $user->can('updateRoles', $this->getRecord())) ->recordTitle(fn (Model $record) => __('permission.role.' . $record->name)) ->preloadRecordSelect() - ->recordSelectOptionsQuery(fn (Builder $query) => $query->whereIn('name', $user->assignableRoles)) + ->recordSelectOptionsQuery(function (Builder $query) { + /** @var User $targetUser */ + $targetUser = $this->getRecord(); + + // Start with basic role filtering based on user permissions. + $query->whereIn('name', Auth::user()->assignableRoles); + + // If trying to assign staff developer roles, ensure the user has Role::DEVELOPER. + $staffRoles = [Role::QUALITY_ASSURANCE, Role::DEV_COMPLIANCE, Role::CODE_REVIEWER]; + if (!$targetUser->hasRole(Role::DEVELOPER)) { + $query->whereNotIn('name', $staffRoles); + } + + return $query; + }) ->after(function ($data) { /** @var User $targetUser */ $targetUser = $this->getRecord(); diff --git a/app/Models/Role.php b/app/Models/Role.php index 65e928a2f2..f34cf76c72 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -5,7 +5,6 @@ namespace App\Models; use Fico7489\Laravel\Pivot\Traits\PivotEventTrait; -use Illuminate\Support\Facades\Auth; use Spatie\Permission\Models\Role as SpatieRole; use Spatie\TypeScriptTransformer\Attributes\TypeScript; @@ -140,13 +139,15 @@ public static function boot() // record users role attach/detach in audit log - /** @var User $user */ - $user = Auth::user(); - static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotIdsAttributes) { if ($relationName === 'users') { foreach ($pivotIds as $pivotId) { $user = User::find($pivotId); + if (!$user) { + // can potentially happen if done by a side effect + return; + } + activity()->causedBy($user)->performedOn($user) ->withProperty('relationships', ['roles' => [$model->id]]) ->withProperty('attributes', ['roles' => [$model->id => []]]) @@ -160,6 +161,11 @@ public static function boot() if ($relationName === 'users') { foreach ($pivotIds as $pivotId) { $user = User::find($pivotId); + if (!$user) { + // can potentially happen if done by a side effect + return; + } + activity()->causedBy($user)->performedOn($user) ->withProperty('relationships', ['roles' => [$model->id]]) ->event('pivotDetached') diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index 76881414a8..1b48b22f02 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -16,19 +16,7 @@ class UserPolicy public function manage(User $user): bool { - return $user->hasAnyRole([ - // admins - Role::ROOT, - Role::ADMINISTRATOR, - - // moderation - Role::MODERATOR, - - // staff developers - Role::CODE_REVIEWER, - Role::DEV_COMPLIANCE, - Role::QUALITY_ASSURANCE, - ]); + return $this->requireAdministrativePrivileges($user); } public function viewAny(?User $user): bool From 23bcf6bc0ec1f4d23cf794708c9865b462a63d17 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Mon, 30 Dec 2024 14:31:44 -0500 Subject: [PATCH 6/8] chore: phpstan --- app/Policies/GameSetPolicy.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Policies/GameSetPolicy.php b/app/Policies/GameSetPolicy.php index f3e636fe68..9e3c0ac88f 100644 --- a/app/Policies/GameSetPolicy.php +++ b/app/Policies/GameSetPolicy.php @@ -70,7 +70,6 @@ public function toggleHasMatureContent(User $user, GameSet $gameSet): bool { return $user->hasAnyRole([ Role::ADMINISTRATOR, - Role::DEVELOPER_STAFF, ]); } } From 1f8fae118ed73a057fe48ef916b36da248d50d18 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Tue, 31 Dec 2024 19:04:52 -0500 Subject: [PATCH 7/8] fix: address pr feedback --- app/Models/Role.php | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/app/Models/Role.php b/app/Models/Role.php index f34cf76c72..f50fc53ccc 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -5,6 +5,7 @@ namespace App\Models; use Fico7489\Laravel\Pivot\Traits\PivotEventTrait; +use Illuminate\Support\Facades\Auth; use Spatie\Permission\Models\Role as SpatieRole; use Spatie\TypeScriptTransformer\Attributes\TypeScript; @@ -143,12 +144,7 @@ public static function boot() if ($relationName === 'users') { foreach ($pivotIds as $pivotId) { $user = User::find($pivotId); - if (!$user) { - // can potentially happen if done by a side effect - return; - } - - activity()->causedBy($user)->performedOn($user) + activity()->causedBy(Auth::user())->performedOn($user) ->withProperty('relationships', ['roles' => [$model->id]]) ->withProperty('attributes', ['roles' => [$model->id => []]]) ->event('pivotAttached') @@ -161,12 +157,7 @@ public static function boot() if ($relationName === 'users') { foreach ($pivotIds as $pivotId) { $user = User::find($pivotId); - if (!$user) { - // can potentially happen if done by a side effect - return; - } - - activity()->causedBy($user)->performedOn($user) + activity()->causedBy(Auth::user())->performedOn($user) ->withProperty('relationships', ['roles' => [$model->id]]) ->event('pivotDetached') ->log('pivotDetached'); From 9fd8146338148041de0c0af8eaea302e0d0c7624 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Tue, 31 Dec 2024 19:09:05 -0500 Subject: [PATCH 8/8] fix: remove dead code --- app/Policies/AchievementPolicy.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Policies/AchievementPolicy.php b/app/Policies/AchievementPolicy.php index 082044281b..562cb9bdbe 100644 --- a/app/Policies/AchievementPolicy.php +++ b/app/Policies/AchievementPolicy.php @@ -97,7 +97,6 @@ public function delete(User $user, Achievement $achievement): bool return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); } @@ -106,7 +105,6 @@ public function restore(User $user, Achievement $achievement): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - Role::DEVELOPER_STAFF, Role::DEVELOPER, ]); }