Skip to content

Commit

Permalink
feat: allow users to change their names (#3071)
Browse files Browse the repository at this point in the history
  • Loading branch information
wescopeland authored Jan 25, 2025
1 parent 4bbf6b0 commit 6c00463
Show file tree
Hide file tree
Showing 32 changed files with 1,273 additions and 22 deletions.
28 changes: 28 additions & 0 deletions app/Community/Actions/ApproveNewDisplayNameAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace App\Community\Actions;

use App\Models\User;
use App\Models\UserUsername;

class ApproveNewDisplayNameAction
{
public function execute(User $user, UserUsername $changeRequest): void
{
// Automatically mark conflicting requests as denied.
UserUsername::where('username', $changeRequest->username)
->where('id', '!=', $changeRequest->id)
->whereNull('approved_at')
->whereNull('denied_at')
->update(['denied_at' => now()]);

$changeRequest->update(['approved_at' => now()]);

$user->display_name = $changeRequest->username;
$user->save();

sendDisplayNameChangeConfirmationEmail($user, $changeRequest->username);
}
}
28 changes: 28 additions & 0 deletions app/Community/Controllers/UserSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Community\Controllers;

use App\Community\Data\StoreUsernameChangeData;
use App\Community\Data\UpdateEmailData;
use App\Community\Data\UpdateLocaleData;
use App\Community\Data\UpdatePasswordData;
Expand All @@ -13,6 +14,7 @@
use App\Community\Enums\ArticleType;
use App\Community\Requests\ResetConnectApiKeyRequest;
use App\Community\Requests\ResetWebApiKeyRequest;
use App\Community\Requests\StoreUsernameChangeRequest;
use App\Community\Requests\UpdateEmailRequest;
use App\Community\Requests\UpdateLocaleRequest;
use App\Community\Requests\UpdatePasswordRequest;
Expand All @@ -26,6 +28,7 @@
use App\Http\Controller;
use App\Models\Role;
use App\Models\User;
use App\Models\UserUsername;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -56,11 +59,18 @@ public function show(): InertiaResponse
);

$can = UserPermissionsData::fromUser($user)->include(
'createUsernameChangeRequest',
'manipulateApiKeys',
'updateAvatar',
'updateMotto'
);

$requestedUsername = UserUsername::whereUserId($user->id)
->pending()
->latest('created_at')
->first()
?->username;

/** @var Collection<int, Role> $displayableRoles */
$displayableRoles = $user->roles;

Expand All @@ -72,11 +82,29 @@ public function show(): InertiaResponse
$userSettings,
$can,
$mappedRoles,
$requestedUsername
);

return Inertia::render('settings', $props);
}

public function storeUsernameChangeRequest(StoreUsernameChangeRequest $request): JsonResponse
{
$this->authorize('create', UserUsername::class);

$data = StoreUsernameChangeData::fromRequest($request);

/** @var User $user */
$user = $request->user();

UserUsername::create([
'user_id' => $user->id,
'username' => $data->newDisplayName,
]);

return response()->json(['success' => true]);
}

public function updatePassword(UpdatePasswordRequest $request): JsonResponse
{
$data = UpdatePasswordData::fromRequest($request);
Expand Down
23 changes: 23 additions & 0 deletions app/Community/Data/StoreUsernameChangeData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace App\Community\Data;

use App\Community\Requests\StoreUsernameChangeRequest;
use Spatie\LaravelData\Data;

class StoreUsernameChangeData extends Data
{
public function __construct(
public string $newDisplayName
) {
}

public static function fromRequest(StoreUsernameChangeRequest $request): self
{
return new self(
newDisplayName: $request->newDisplayName,
);
}
}
1 change: 1 addition & 0 deletions app/Community/Data/UserSettingsPagePropsData.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function __construct(
public UserPermissionsData $can,
/** @var RoleData[] */
public array $displayableRoles,
public ?string $requestedUsername = null,
) {
}
}
18 changes: 18 additions & 0 deletions app/Community/Requests/StoreUsernameChangeRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace App\Community\Requests;

use App\Support\Rules\ValidNewUsername;
use Illuminate\Foundation\Http\FormRequest;

class StoreUsernameChangeRequest extends FormRequest
{
public function rules(): array
{
return [
'newDisplayName' => ValidNewUsername::get($this->user()),
];
}
}
3 changes: 3 additions & 0 deletions app/Community/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ protected function mapWebRoutes(): void
Route::put('password', [UserSettingsController::class, 'updatePassword'])->name('api.settings.password.update');
Route::put('email', [UserSettingsController::class, 'updateEmail'])->name('api.settings.email.update');

Route::post('username-change-request', [UserSettingsController::class, 'storeUsernameChangeRequest'])
->name('api.settings.username-change-request.store');

Route::delete('keys/web', [UserSettingsController::class, 'resetWebApiKey'])->name('api.settings.keys.web.destroy');
Route::delete('keys/connect', [UserSettingsController::class, 'resetConnectApiKey'])->name('api.settings.keys.connect.destroy');
});
Expand Down
2 changes: 2 additions & 0 deletions app/Data/UserData.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
public Lazy|array|null $displayableRoles = null,
public Lazy|string|null $emailAddress = null,
public Lazy|int $id = 0,
public Lazy|bool $isEmailVerified = false,
public Lazy|bool $isMuted = false,
public Lazy|bool $isNew = false,
public Lazy|int|null $legacyPermissions = null,
Expand Down Expand Up @@ -78,6 +79,7 @@ public static function fromUser(User $user): self
emailAddress: Lazy::create(fn () => $user->EmailAddress),
mutedUntil: Lazy::create(fn () => $user->muted_until),
id: Lazy::create(fn () => $user->id),
isEmailVerified: Lazy::create(fn () => $user->isEmailVerified()),
isMuted: Lazy::create(fn () => $user->isMuted()),
isNew: Lazy::create(fn () => $user->isNew()),
legacyPermissions: Lazy::create(fn () => (int) $user->getAttribute('Permissions')),
Expand Down
2 changes: 2 additions & 0 deletions app/Data/UserPermissionsData.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class UserPermissionsData extends Data
{
public function __construct(
public Lazy|bool $createTriggerTicket,
public Lazy|bool $createUsernameChangeRequest,
public Lazy|bool $develop,
public Lazy|bool $manageGameHashes,
public Lazy|bool $manageGameSets,
Expand All @@ -35,6 +36,7 @@ public static function fromUser(
? $user->can('createFor', [\App\Models\TriggerTicket::class, $triggerable])
: $user?->can('create', \App\Models\TriggerTicket::class) ?? false
),
createUsernameChangeRequest: Lazy::create(fn () => $user ? $user->can('create', \App\Models\UserUsername::class) : false),
develop: Lazy::create(fn () => $user ? $user->can('develop') : false),
manageGameHashes: Lazy::create(fn () => $user ? $user->can('manage', \App\Models\GameHash::class) : false),
manageGameSets: Lazy::create(fn () => $user ? $user->can('manage', \App\Models\GameSet::class) : false),
Expand Down
187 changes: 187 additions & 0 deletions app/Filament/Resources/UserUsernameResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources;

use App\Community\Actions\ApproveNewDisplayNameAction;
use App\Filament\Extensions\Resources\Resource;
use App\Filament\Resources\UserUsernameResource\Pages;
use App\Models\User;
use App\Models\UserUsername;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

class UserUsernameResource extends Resource
{
protected static ?string $model = UserUsername::class;

protected static ?int $navigationSort = 30;

protected static ?string $navigationIcon = 'heroicon-s-wrench';

protected static ?string $navigationGroup = 'Tools';

protected static ?string $navigationLabel = 'Username Change Requests';

protected static ?string $modelLabel = 'Username Change Request';

public static function getNavigationBadge(): ?string
{
$count = static::getModel()::pending()->count();

return "{$count}";
}

public static function getNavigationBadgeColor(): ?string
{
return static::getNavigationBadge() > 0 ? 'warning' : null;
}

public static function form(Form $form): Form
{
return $form
->schema([

]);
}

public static function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn (Builder $query) => $query->where(function ($query) {
$query->whereNotNull('approved_at')
->orWhereNotNull('denied_at')
->orWhere('created_at', '>', now()->subDays(30));
}))
->columns([
Tables\Columns\TextColumn::make('user.username')
->label('Original Username')
->url(fn (UserUsername $record) => UserResource::getUrl('view', ['record' => $record->user->display_name]))
->extraAttributes(['class' => 'underline'])
->openUrlInNewTab()
->searchable()
->sortable(),

Tables\Columns\TextColumn::make('user.display_name')
->label('Current Username')
->url(fn (UserUsername $record) => UserResource::getUrl('view', ['record' => $record->user->display_name]))
->extraAttributes(['class' => 'underline'])
->openUrlInNewTab()
->searchable()
->sortable(),

Tables\Columns\TextColumn::make('username')
->label('Requested New Username')
->searchable()
->sortable(),

Tables\Columns\TextColumn::make('created_at')
->label('Requested At')
->dateTime()
->sortable(),

Tables\Columns\TextColumn::make('status')
->label('Status')
->state(fn (UserUsername $record): string => match (true) {
$record->is_approved => 'Approved',
$record->is_denied => 'Denied',
default => 'Pending',
})
->icon(fn (UserUsername $record): string => match (true) {
$record->is_approved => 'heroicon-o-check-circle',
$record->is_denied => 'heroicon-o-x-circle',
default => 'heroicon-o-clock',
})
->color(fn (UserUsername $record): string => match (true) {
$record->is_approved => 'success',
$record->is_denied => 'danger',
default => 'warning',
}),
])
->defaultSort('created_at', 'desc')
->filters([
Tables\Filters\SelectFilter::make('status')
->options([
'pending' => 'Pending',
'approved' => 'Approved',
'denied' => 'Denied',
])
->query(function ($query, $state) {
if (!isset($state['value'])) {
return $query;
}

return match ($state['value']) {
'pending' => $query->pending(),
'approved' => $query->approved(),
'denied' => $query->denied(),
default => $query,
};
})
->default('pending'),
])
->actions([
Tables\Actions\Action::make('approve')
->action(function (UserUsername $record) {
/** @var User $user */
$user = $record->user;
$originalDisplayName = $user->display_name;

(new ApproveNewDisplayNameAction())->execute($user, $record);

Notification::make()
->success()
->title('Success')
->body("Approved {$originalDisplayName}'s username change request.")
->send();
})
->visible(fn (UserUsername $record) => !$record->is_approved && !$record->is_denied)
->requiresConfirmation()
->modalDescription("Are you sure you'd like to do this? The username change will go into effect immediately.")
->color('success')
->icon('heroicon-o-check'),

Tables\Actions\Action::make('deny')
->action(function (UserUsername $record) {
$record->update(['denied_at' => now()]);

/** @var User $user */
$user = $record->user;

sendDisplayNameChangeDeclineEmail($user, $record->username);

Notification::make()
->success()
->title('Success')
->body("Denied {$record->user->display_name}'s username change request.")
->send();
})
->visible(fn (UserUsername $record) => !$record->is_approved && !$record->is_denied)
->requiresConfirmation()
->modalDescription('Are you sure you want to deny this username change request?')
->color('danger')
->icon('heroicon-o-x-mark'),
])
->bulkActions([

]);
}

public static function getRelations(): array
{
return [

];
}

public static function getPages(): array
{
return [
'index' => Pages\Index::route('/'),
];
}
}
Loading

0 comments on commit 6c00463

Please sign in to comment.