Add toggle for externally managed users (#1825)

This commit is contained in:
Boy132
2025-12-17 20:09:17 +01:00
committed by GitHub
parent be55e75109
commit 9d1e7f510f
13 changed files with 109 additions and 57 deletions

View File

@@ -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,65 +225,71 @@ 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(),
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()),
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(),
Select::make('language')
->label(trans('profile.language'))
->required()
->prefixIcon('tabler-flag')
->live()
->default('en')
->searchable()
->selectablePlaceholder(false)
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
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);

View File

@@ -93,12 +93,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()
@@ -106,6 +108,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')
@@ -535,7 +538,6 @@ class EditProfile extends BaseEditProfile
]),
]),
]),
])
->operation('edit')
->model($this->getUser())

View File

@@ -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?',
];
}
}

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -49,6 +49,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
@@ -118,6 +119,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/
protected $fillable = [
'external_id',
'is_managed_externally',
'username',
'email',
'password',
@@ -140,6 +142,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,
@@ -154,6 +157,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'],
@@ -175,6 +179,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
protected function casts(): array
{
return [
'is_managed_externally' => 'boolean',
'mfa_app_secret' => 'encrypted',
'mfa_app_recovery_codes' => 'encrypted:array',
'oauth' => 'array',

View File

@@ -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,

View File

@@ -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',

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',
'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',

View File

@@ -23,7 +23,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase
$response->assertJsonStructure([
'object',
'attributes' => [
'id', 'external_id', 'uuid', 'username', 'email',
'id', 'external_id', 'is_managed_externally', 'uuid', 'username', 'email',
'language', 'root_admin', '2fa', 'created_at', 'updated_at',
],
]);
@@ -33,6 +33,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase
'attributes' => [
'id' => $user->id,
'external_id' => $user->external_id,
'is_managed_externally' => $user->is_managed_externally,
'uuid' => $user->uuid,
'username' => $user->username,
'email' => $user->email,

View File

@@ -26,8 +26,8 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
$response->assertJsonStructure([
'object',
'data' => [
['object', 'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa_enabled', '2fa', 'created_at', 'updated_at']],
['object', 'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa_enabled', '2fa', 'created_at', 'updated_at']],
['object', 'attributes' => ['id', 'external_id', 'is_managed_externally', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa_enabled', '2fa', 'created_at', 'updated_at']],
['object', 'attributes' => ['id', 'external_id', 'is_managed_externally', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa_enabled', '2fa', 'created_at', 'updated_at']],
],
'meta' => ['pagination' => ['total', 'count', 'per_page', 'current_page', 'total_pages']],
]);
@@ -51,6 +51,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
'attributes' => [
'id' => $this->getApiUser()->id,
'external_id' => $this->getApiUser()->external_id,
'is_managed_externally' => $this->getApiUser()->is_managed_externally,
'uuid' => $this->getApiUser()->uuid,
'username' => $this->getApiUser()->username,
'email' => $this->getApiUser()->email,
@@ -67,6 +68,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
'attributes' => [
'id' => $user->id,
'external_id' => $user->external_id,
'is_managed_externally' => $user->is_managed_externally,
'uuid' => $user->uuid,
'username' => $user->username,
'email' => $user->email,
@@ -92,7 +94,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
$response->assertJsonCount(2);
$response->assertJsonStructure([
'object',
'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'],
'attributes' => ['id', 'external_id', 'is_managed_externally', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'],
]);
$response->assertJson([
@@ -100,6 +102,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
'attributes' => [
'id' => $user->id,
'external_id' => $user->external_id,
'is_managed_externally' => $user->is_managed_externally,
'uuid' => $user->uuid,
'username' => $user->username,
'email' => $user->email,
@@ -126,7 +129,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
$response->assertJsonStructure([
'object',
'attributes' => [
'id', 'external_id', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at',
'id', 'external_id', 'is_managed_externally', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at',
'relationships' => ['servers' => ['object', 'data' => [['object', 'attributes' => []]]]],
],
]);
@@ -213,7 +216,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
$response->assertJsonCount(3);
$response->assertJsonStructure([
'object',
'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'],
'attributes' => ['id', 'external_id', 'is_managed_externally', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'],
'meta' => ['resource'],
]);
@@ -244,7 +247,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
$response->assertJsonCount(2);
$response->assertJsonStructure([
'object',
'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'],
'attributes' => ['id', 'external_id', 'is_managed_externally', 'uuid', 'username', 'email', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'],
]);
$this->assertDatabaseHas('users', ['username' => 'new.test.name', 'email' => 'new@emailtest.com']);