add toggle for externally managed users

This commit is contained in:
Boy132 2025-10-23 09:53:48 +02:00
parent 8e006ac32d
commit a9165c66f7
10 changed files with 98 additions and 51 deletions

View File

@ -33,6 +33,7 @@ use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Pages\PageRegistration;
@ -224,67 +225,73 @@ class UserResource extends Resource
'md' => 1, 'md' => 1,
'lg' => 1, 'lg' => 1,
]), ]),
Select::make('timezone') Toggle::make('is_managed_externally')
->label(trans('profile.timezone')) ->label(trans('admin/user.is_managed_externally'))
->hintIcon('tabler-question-mark', trans('admin/user.is_managed_externally_helper'))
->inline(false)
->columnSpan([ ->columnSpan([
'default' => 1, 'default' => 1,
'md' => 1, 'md' => 1,
'lg' => 1, 'lg' => 1,
]) ]),
->required() Section::make(trans('profile.tabs.customization'))
->prefixIcon('tabler-clock-pin') ->collapsible()
->default(fn () => config('app.timezone', 'UTC')) ->columnSpanFull()
->selectablePlaceholder(false) ->columns(2)
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz])) ->schema([
->searchable() Select::make('timezone')
->native(false), ->label(trans('profile.timezone'))
Select::make('language') ->required()
->label(trans('profile.language')) ->prefixIcon('tabler-clock-pin')
->columnSpan([ ->default(fn () => config('app.timezone', 'UTC'))
'default' => 1, ->selectablePlaceholder(false)
'md' => 1, ->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
'lg' => 1, ->searchable()
]) ->native(false),
->required() Select::make('language')
->prefixIcon('tabler-flag') ->label(trans('profile.language'))
->live() ->required()
->default('en') ->prefixIcon('tabler-flag')
->searchable() ->live()
->selectablePlaceholder(false) ->default('en')
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()) ->searchable()
->native(false), ->selectablePlaceholder(false)
FileUpload::make('avatar') ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false) ->native(false),
->avatar() FileUpload::make('avatar')
->directory('avatars') ->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false)
->disk('public') ->columnSpanFull()
->formatStateUsing(function (FileUpload $fileUpload, ?User $user) { ->avatar()
if (!$user) { ->directory('avatars')
return null; ->disk('public')
} ->formatStateUsing(function (FileUpload $fileUpload, ?User $user) {
$path = $fileUpload->getDirectory() . '/' . $user->id . '.png'; if (!$user) {
if ($fileUpload->getDisk()->exists($path)) { return null;
return $path; }
} $path = $fileUpload->getDirectory() . '/' . $user->id . '.png';
}) if ($fileUpload->getDisk()->exists($path)) {
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) { return $path;
if ($file instanceof TemporaryUploadedFile) { }
return $file->delete(); })
} ->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
if ($file instanceof TemporaryUploadedFile) {
return $file->delete();
}
if ($fileUpload->getDisk()->exists($file)) { if ($fileUpload->getDisk()->exists($file)) {
return $fileUpload->getDisk()->delete($file); return $fileUpload->getDisk()->delete($file);
} }
}), }),
]),
Section::make(trans('profile.tabs.oauth')) Section::make(trans('profile.tabs.oauth'))
->visible(fn (?User $user) => $user) ->visible(fn (?User $user) => $user)
->collapsible() ->collapsible()
->columnSpanFull() ->columnSpanFull()
->schema(function (OAuthService $oauthService, ?User $user) { ->schema(function (OAuthService $oauthService, ?User $user) {
if (!$user) { if (!$user) {
return; return;
} }
$actions = []; $actions = [];
foreach ($user->oauth ?? [] as $schema => $_) { foreach ($user->oauth ?? [] as $schema => $_) {
$schema = $oauthService->get($schema); $schema = $oauthService->get($schema);

View File

@ -94,12 +94,14 @@ class EditProfile extends BaseEditProfile
->icon('tabler-user-cog') ->icon('tabler-user-cog')
->schema([ ->schema([
TextInput::make('username') TextInput::make('username')
->disabled(fn (User $user) => $user->is_managed_externally)
->prefixIcon('tabler-user') ->prefixIcon('tabler-user')
->label(trans('profile.username')) ->label(trans('profile.username'))
->required() ->required()
->maxLength(255) ->maxLength(255)
->unique(), ->unique(),
TextInput::make('email') TextInput::make('email')
->disabled(fn (User $user) => $user->is_managed_externally)
->prefixIcon('tabler-mail') ->prefixIcon('tabler-mail')
->label(trans('profile.email')) ->label(trans('profile.email'))
->email() ->email()
@ -107,6 +109,7 @@ class EditProfile extends BaseEditProfile
->maxLength(255) ->maxLength(255)
->unique(), ->unique(),
TextInput::make('password') TextInput::make('password')
->hidden(fn (User $user) => $user->is_managed_externally)
->label(trans('profile.password')) ->label(trans('profile.password'))
->password() ->password()
->prefixIcon('tabler-password') ->prefixIcon('tabler-password')
@ -537,7 +540,6 @@ class EditProfile extends BaseEditProfile
]), ]),
]), ]),
]), ]),
]) ])
->operation('edit') ->operation('edit')
->model($this->getUser()) ->model($this->getUser())

View File

@ -22,6 +22,7 @@ class StoreUserRequest extends ApplicationApiRequest
return collect($rules)->only([ return collect($rules)->only([
'external_id', 'external_id',
'is_managed_externally',
'email', 'email',
'username', 'username',
'password', 'password',
@ -39,6 +40,7 @@ class StoreUserRequest extends ApplicationApiRequest
{ {
return [ return [
'external_id' => 'Third Party Identifier', 'external_id' => 'Third Party Identifier',
'is_managed_externally' => 'Is managed by Third Party?',
]; ];
} }
} }

View File

@ -26,7 +26,7 @@ class UpdateEmailRequest extends ClientApiRequest
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password')); throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
} }
return true; return !$this->user()->is_managed_externally;
} }
public function rules(): array public function rules(): array

View File

@ -25,7 +25,7 @@ class UpdatePasswordRequest extends ClientApiRequest
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password')); throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
} }
return true; return !$this->user()->is_managed_externally;
} }
public function rules(): array public function rules(): array

View File

@ -48,6 +48,7 @@ use Spatie\Permission\Traits\HasRoles;
* *
* @property int $id * @property int $id
* @property string|null $external_id * @property string|null $external_id
* @property bool $is_managed_externally
* @property string $uuid * @property string $uuid
* @property string $username * @property string $username
* @property string $email * @property string $email
@ -117,6 +118,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/ */
protected $fillable = [ protected $fillable = [
'external_id', 'external_id',
'is_managed_externally',
'username', 'username',
'email', 'email',
'password', 'password',
@ -139,6 +141,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/ */
protected $attributes = [ protected $attributes = [
'external_id' => null, 'external_id' => null,
'is_managed_externally' => false,
'language' => 'en', 'language' => 'en',
'timezone' => 'UTC', 'timezone' => 'UTC',
'mfa_app_secret' => null, 'mfa_app_secret' => null,
@ -153,6 +156,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'uuid' => ['nullable', 'string', 'size:36', 'unique:users,uuid'], 'uuid' => ['nullable', 'string', 'size:36', 'unique:users,uuid'],
'email' => ['required', 'email', 'between:1,255', 'unique:users,email'], 'email' => ['required', 'email', 'between:1,255', 'unique:users,email'],
'external_id' => ['sometimes', 'nullable', 'string', 'max:255', 'unique:users,external_id'], 'external_id' => ['sometimes', 'nullable', 'string', 'max:255', 'unique:users,external_id'],
'is_managed_externally' => ['boolean'],
'username' => ['required', 'between:1,255', 'unique:users,username'], 'username' => ['required', 'between:1,255', 'unique:users,username'],
'password' => ['sometimes', 'nullable', 'string'], 'password' => ['sometimes', 'nullable', 'string'],
'language' => ['string'], 'language' => ['string'],

View File

@ -31,6 +31,7 @@ class UserTransformer extends BaseTransformer
return [ return [
'id' => $user->id, 'id' => $user->id,
'external_id' => $user->external_id, 'external_id' => $user->external_id,
'is_managed_externally' => $user->is_managed_externally,
'uuid' => $user->uuid, 'uuid' => $user->uuid,
'username' => $user->username, 'username' => $user->username,
'email' => $user->email, 'email' => $user->email,

View File

@ -26,6 +26,7 @@ class UserFactory extends Factory
return [ return [
'external_id' => null, 'external_id' => null,
'is_managed_externally' => false,
'uuid' => Uuid::uuid4()->toString(), 'uuid' => Uuid::uuid4()->toString(),
'username' => $this->faker->userName() . '_' . Str::random(10), 'username' => $this->faker->userName() . '_' . Str::random(10),
'email' => Str::random(32) . '@example.com', 'email' => Str::random(32) . '@example.com',

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -10,6 +10,8 @@ return [
'username' => 'Username', 'username' => 'Username',
'password' => 'Password', 'password' => 'Password',
'external_id' => 'External ID', '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.', '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', 'admin_roles' => 'Admin Roles',
'roles' => 'Roles', 'roles' => 'Roles',