mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Add toggle for externally managed users (#1825)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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?',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user