From a9165c66f76fbb834a78d2d25b580d7810fd2b94 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 23 Oct 2025 09:53:48 +0200 Subject: [PATCH] add toggle for externally managed users --- .../Admin/Resources/Users/UserResource.php | 103 ++++++++++-------- app/Filament/Pages/Auth/EditProfile.php | 4 +- .../Application/Users/StoreUserRequest.php | 2 + .../Api/Client/Account/UpdateEmailRequest.php | 2 +- .../Client/Account/UpdatePasswordRequest.php | 2 +- app/Models/User.php | 4 + .../Api/Application/UserTransformer.php | 1 + database/Factories/UserFactory.php | 1 + ...209_add_is_managed_externally_to_users.php | 28 +++++ lang/en/admin/user.php | 2 + 10 files changed, 98 insertions(+), 51 deletions(-) create mode 100644 database/migrations/2025_10_23_073209_add_is_managed_externally_to_users.php diff --git a/app/Filament/Admin/Resources/Users/UserResource.php b/app/Filament/Admin/Resources/Users/UserResource.php index 052924d66..12dec25f1 100644 --- a/app/Filament/Admin/Resources/Users/UserResource.php +++ b/app/Filament/Admin/Resources/Users/UserResource.php @@ -33,6 +33,7 @@ use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; use Filament\Resources\Pages\PageRegistration; @@ -224,67 +225,73 @@ class UserResource extends Resource 'md' => 1, 'lg' => 1, ]), - Select::make('timezone') - ->label(trans('profile.timezone')) + Toggle::make('is_managed_externally') + ->label(trans('admin/user.is_managed_externally')) + ->hintIcon('tabler-question-mark', trans('admin/user.is_managed_externally_helper')) + ->inline(false) ->columnSpan([ 'default' => 1, 'md' => 1, 'lg' => 1, - ]) - ->required() - ->prefixIcon('tabler-clock-pin') - ->default(fn () => config('app.timezone', 'UTC')) - ->selectablePlaceholder(false) - ->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz])) - ->searchable() - ->native(false), - Select::make('language') - ->label(trans('profile.language')) - ->columnSpan([ - 'default' => 1, - 'md' => 1, - 'lg' => 1, - ]) - ->required() - ->prefixIcon('tabler-flag') - ->live() - ->default('en') - ->searchable() - ->selectablePlaceholder(false) - ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()) - ->native(false), - FileUpload::make('avatar') - ->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false) - ->avatar() - ->directory('avatars') - ->disk('public') - ->formatStateUsing(function (FileUpload $fileUpload, ?User $user) { - if (!$user) { - return null; - } - $path = $fileUpload->getDirectory() . '/' . $user->id . '.png'; - if ($fileUpload->getDisk()->exists($path)) { - return $path; - } - }) - ->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) { - if ($file instanceof TemporaryUploadedFile) { - return $file->delete(); - } + ]), + Section::make(trans('profile.tabs.customization')) + ->collapsible() + ->columnSpanFull() + ->columns(2) + ->schema([ + Select::make('timezone') + ->label(trans('profile.timezone')) + ->required() + ->prefixIcon('tabler-clock-pin') + ->default(fn () => config('app.timezone', 'UTC')) + ->selectablePlaceholder(false) + ->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz])) + ->searchable() + ->native(false), + Select::make('language') + ->label(trans('profile.language')) + ->required() + ->prefixIcon('tabler-flag') + ->live() + ->default('en') + ->searchable() + ->selectablePlaceholder(false) + ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()) + ->native(false), + FileUpload::make('avatar') + ->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false) + ->columnSpanFull() + ->avatar() + ->directory('avatars') + ->disk('public') + ->formatStateUsing(function (FileUpload $fileUpload, ?User $user) { + if (!$user) { + return null; + } + $path = $fileUpload->getDirectory() . '/' . $user->id . '.png'; + if ($fileUpload->getDisk()->exists($path)) { + return $path; + } + }) + ->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) { + if ($file instanceof TemporaryUploadedFile) { + return $file->delete(); + } - if ($fileUpload->getDisk()->exists($file)) { - return $fileUpload->getDisk()->delete($file); - } - }), + if ($fileUpload->getDisk()->exists($file)) { + return $fileUpload->getDisk()->delete($file); + } + }), + ]), Section::make(trans('profile.tabs.oauth')) ->visible(fn (?User $user) => $user) ->collapsible() ->columnSpanFull() ->schema(function (OAuthService $oauthService, ?User $user) { - if (!$user) { return; } + $actions = []; foreach ($user->oauth ?? [] as $schema => $_) { $schema = $oauthService->get($schema); diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index 86d811af3..8f0059820 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -94,12 +94,14 @@ class EditProfile extends BaseEditProfile ->icon('tabler-user-cog') ->schema([ TextInput::make('username') + ->disabled(fn (User $user) => $user->is_managed_externally) ->prefixIcon('tabler-user') ->label(trans('profile.username')) ->required() ->maxLength(255) ->unique(), TextInput::make('email') + ->disabled(fn (User $user) => $user->is_managed_externally) ->prefixIcon('tabler-mail') ->label(trans('profile.email')) ->email() @@ -107,6 +109,7 @@ class EditProfile extends BaseEditProfile ->maxLength(255) ->unique(), TextInput::make('password') + ->hidden(fn (User $user) => $user->is_managed_externally) ->label(trans('profile.password')) ->password() ->prefixIcon('tabler-password') @@ -537,7 +540,6 @@ class EditProfile extends BaseEditProfile ]), ]), ]), - ]) ->operation('edit') ->model($this->getUser()) diff --git a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php index cf3f4c7de..520e36c6b 100644 --- a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php @@ -22,6 +22,7 @@ class StoreUserRequest extends ApplicationApiRequest return collect($rules)->only([ 'external_id', + 'is_managed_externally', 'email', 'username', 'password', @@ -39,6 +40,7 @@ class StoreUserRequest extends ApplicationApiRequest { return [ 'external_id' => 'Third Party Identifier', + 'is_managed_externally' => 'Is managed by Third Party?', ]; } } diff --git a/app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php b/app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php index 4d58055c7..68bdb31d7 100644 --- a/app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php +++ b/app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php @@ -26,7 +26,7 @@ class UpdateEmailRequest extends ClientApiRequest throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password')); } - return true; + return !$this->user()->is_managed_externally; } public function rules(): array diff --git a/app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php b/app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php index ad305e4d6..afbbc1f2f 100644 --- a/app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php +++ b/app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php @@ -25,7 +25,7 @@ class UpdatePasswordRequest extends ClientApiRequest throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password')); } - return true; + return !$this->user()->is_managed_externally; } public function rules(): array diff --git a/app/Models/User.php b/app/Models/User.php index 2a115bdcc..ee9961518 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -48,6 +48,7 @@ use Spatie\Permission\Traits\HasRoles; * * @property int $id * @property string|null $external_id + * @property bool $is_managed_externally * @property string $uuid * @property string $username * @property string $email @@ -117,6 +118,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac */ protected $fillable = [ 'external_id', + 'is_managed_externally', 'username', 'email', 'password', @@ -139,6 +141,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac */ protected $attributes = [ 'external_id' => null, + 'is_managed_externally' => false, 'language' => 'en', 'timezone' => 'UTC', 'mfa_app_secret' => null, @@ -153,6 +156,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'uuid' => ['nullable', 'string', 'size:36', 'unique:users,uuid'], 'email' => ['required', 'email', 'between:1,255', 'unique:users,email'], 'external_id' => ['sometimes', 'nullable', 'string', 'max:255', 'unique:users,external_id'], + 'is_managed_externally' => ['boolean'], 'username' => ['required', 'between:1,255', 'unique:users,username'], 'password' => ['sometimes', 'nullable', 'string'], 'language' => ['string'], diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php index da45df2ae..f3da7cc49 100644 --- a/app/Transformers/Api/Application/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -31,6 +31,7 @@ class UserTransformer extends BaseTransformer return [ 'id' => $user->id, 'external_id' => $user->external_id, + 'is_managed_externally' => $user->is_managed_externally, 'uuid' => $user->uuid, 'username' => $user->username, 'email' => $user->email, diff --git a/database/Factories/UserFactory.php b/database/Factories/UserFactory.php index 2ac70cc1d..0d43631c6 100644 --- a/database/Factories/UserFactory.php +++ b/database/Factories/UserFactory.php @@ -26,6 +26,7 @@ class UserFactory extends Factory return [ 'external_id' => null, + 'is_managed_externally' => false, 'uuid' => Uuid::uuid4()->toString(), 'username' => $this->faker->userName() . '_' . Str::random(10), 'email' => Str::random(32) . '@example.com', diff --git a/database/migrations/2025_10_23_073209_add_is_managed_externally_to_users.php b/database/migrations/2025_10_23_073209_add_is_managed_externally_to_users.php new file mode 100644 index 000000000..24f171fb7 --- /dev/null +++ b/database/migrations/2025_10_23_073209_add_is_managed_externally_to_users.php @@ -0,0 +1,28 @@ +boolean('is_managed_externally')->default(false)->after('external_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('is_managed_externally'); + }); + } +}; diff --git a/lang/en/admin/user.php b/lang/en/admin/user.php index 5059f238f..62081d28d 100644 --- a/lang/en/admin/user.php +++ b/lang/en/admin/user.php @@ -10,6 +10,8 @@ return [ 'username' => 'Username', 'password' => 'Password', 'external_id' => 'External ID', + 'is_managed_externally' => 'Is managed externally?', + 'is_managed_externally_helper' => 'If your users are managed by external software (e.g. a billing software) you may enable this to prevent users from changing their username, e-mail and password from within the panel.', 'password_help' => 'Providing a user password is optional. New user email will prompt users to create a password the first time they login.', 'admin_roles' => 'Admin Roles', 'roles' => 'Roles',