From fa8ae0aea5b69220ba1ba6e2e348a8deddc765ff Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 7 Apr 2025 16:06:19 +0200 Subject: [PATCH] Add avatar providers (#1192) * Add avatar providers * fix exists check for local avatar * Use avatar in user lists --------- Co-authored-by: Charles --- app/Extensions/Avatar/AvatarProvider.php | 40 ++++++++++++++ .../Avatar/Providers/GravatarProvider.php | 27 ++++++++++ .../Avatar/Providers/LocalAvatarProvider.php | 29 ++++++++++ .../Avatar/Providers/UiAvatarsProvider.php | 31 +++++++++++ app/Filament/Admin/Pages/Settings.php | 53 ++++++++++++------- app/Filament/Admin/Resources/UserResource.php | 6 ++- app/Filament/Pages/Auth/EditProfile.php | 6 +++ .../Server/Resources/UserResource.php | 4 +- app/Models/ActivityLog.php | 5 +- app/Models/User.php | 12 +---- app/Providers/AppServiceProvider.php | 8 +++ app/Providers/Filament/AdminPanelProvider.php | 2 + app/Providers/Filament/AppPanelProvider.php | 2 + .../Filament/ServerPanelProvider.php | 2 + config/panel.php | 1 + ...8_104348_drop_gravatar_column_in_users.php | 28 ++++++++++ lang/en/admin/setting.php | 1 + 17 files changed, 221 insertions(+), 36 deletions(-) create mode 100644 app/Extensions/Avatar/AvatarProvider.php create mode 100644 app/Extensions/Avatar/Providers/GravatarProvider.php create mode 100644 app/Extensions/Avatar/Providers/LocalAvatarProvider.php create mode 100644 app/Extensions/Avatar/Providers/UiAvatarsProvider.php create mode 100644 database/migrations/2025_03_28_104348_drop_gravatar_column_in_users.php diff --git a/app/Extensions/Avatar/AvatarProvider.php b/app/Extensions/Avatar/AvatarProvider.php new file mode 100644 index 000000000..2c707e696 --- /dev/null +++ b/app/Extensions/Avatar/AvatarProvider.php @@ -0,0 +1,40 @@ + + */ + protected static array $providers = []; + + public static function getProvider(string $id): ?self + { + return Arr::get(static::$providers, $id); + } + + /** + * @return array + */ + public static function getAll(): array + { + return static::$providers; + } + + public function __construct() + { + static::$providers[$this->getId()] = $this; + } + + abstract public function getId(): string; + + public function getName(): string + { + return Str::title($this->getId()); + } +} diff --git a/app/Extensions/Avatar/Providers/GravatarProvider.php b/app/Extensions/Avatar/Providers/GravatarProvider.php new file mode 100644 index 000000000..6f47a119a --- /dev/null +++ b/app/Extensions/Avatar/Providers/GravatarProvider.php @@ -0,0 +1,27 @@ +email); + } + + public static function register(): self + { + return new self(); + } +} diff --git a/app/Extensions/Avatar/Providers/LocalAvatarProvider.php b/app/Extensions/Avatar/Providers/LocalAvatarProvider.php new file mode 100644 index 000000000..f84cc5c4c --- /dev/null +++ b/app/Extensions/Avatar/Providers/LocalAvatarProvider.php @@ -0,0 +1,29 @@ +getKey() . '.png'; + + return Storage::disk('public')->exists($path) ? Storage::url($path) : (new FilamentUiAvatarsProvider())->get($record); + } + + public static function register(): self + { + return new self(); + } +} diff --git a/app/Extensions/Avatar/Providers/UiAvatarsProvider.php b/app/Extensions/Avatar/Providers/UiAvatarsProvider.php new file mode 100644 index 000000000..e1df16b16 --- /dev/null +++ b/app/Extensions/Avatar/Providers/UiAvatarsProvider.php @@ -0,0 +1,31 @@ +get($record); + } + + public static function register(): self + { + return new self(); + } +} diff --git a/app/Filament/Admin/Pages/Settings.php b/app/Filament/Admin/Pages/Settings.php index 4529996de..026e6a391 100644 --- a/app/Filament/Admin/Pages/Settings.php +++ b/app/Filament/Admin/Pages/Settings.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Pages; +use App\Extensions\Avatar\AvatarProvider; use App\Extensions\Captcha\Providers\CaptchaProvider; use App\Extensions\OAuth\Providers\OAuthProvider; use App\Models\Backup; @@ -134,26 +135,38 @@ class Settings extends Page implements HasForms ->default(env('APP_FAVICON', '/pelican.ico')) ->placeholder('/pelican.ico'), ]), - Toggle::make('APP_DEBUG') - ->label(trans('admin/setting.general.debug_mode')) - ->inline(false) - ->onIcon('tabler-check') - ->offIcon('tabler-x') - ->onColor('success') - ->offColor('danger') - ->formatStateUsing(fn ($state): bool => (bool) $state) - ->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state)) - ->default(env('APP_DEBUG', config('app.debug'))), - ToggleButtons::make('FILAMENT_TOP_NAVIGATION') - ->label(trans('admin/setting.general.navigation')) - ->inline() - ->options([ - false => trans('admin/setting.general.sidebar'), - true => trans('admin/setting.general.topbar'), - ]) - ->formatStateUsing(fn ($state): bool => (bool) $state) - ->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state)) - ->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), + Group::make() + ->columnSpan(2) + ->columns(4) + ->schema([ + Toggle::make('APP_DEBUG') + ->label(trans('admin/setting.general.debug_mode')) + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state)) + ->default(env('APP_DEBUG', config('app.debug'))), + ToggleButtons::make('FILAMENT_TOP_NAVIGATION') + ->label(trans('admin/setting.general.navigation')) + ->inline() + ->options([ + false => trans('admin/setting.general.sidebar'), + true => trans('admin/setting.general.topbar'), + ]) + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state)) + ->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), + Select::make('FILAMENT_AVATAR_PROVIDER') + ->label(trans('admin/setting.general.avatar_provider')) + ->columnSpan(2) + ->native(false) + ->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()])) + ->selectablePlaceholder(false) + ->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))), + ]), ToggleButtons::make('PANEL_USE_BINARY_PREFIX') ->label(trans('admin/setting.general.unit_prefix')) ->inline() diff --git a/app/Filament/Admin/Resources/UserResource.php b/app/Filament/Admin/Resources/UserResource.php index 3291481a6..b62bfec0b 100644 --- a/app/Filament/Admin/Resources/UserResource.php +++ b/app/Filament/Admin/Resources/UserResource.php @@ -6,6 +6,7 @@ use App\Filament\Admin\Resources\UserResource\Pages; use App\Filament\Admin\Resources\UserResource\RelationManagers; use App\Models\Role; use App\Models\User; +use Filament\Facades\Filament; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; @@ -58,8 +59,9 @@ class UserResource extends Resource ImageColumn::make('picture') ->visibleFrom('lg') ->label('') - ->extraImgAttributes(['class' => 'rounded-full']) - ->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))), + ->circular() + ->alignCenter() + ->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)), TextColumn::make('username') ->label(trans('admin/user.username')), TextColumn::make('email') diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index c0f21801e..cdb55c725 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -19,6 +19,7 @@ use chillerlan\QRCode\QROptions; use DateTimeZone; use Filament\Forms\Components\Actions; use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Repeater; @@ -126,6 +127,11 @@ class EditProfile extends BaseEditProfile ->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state]))) ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()) ->native(false), + FileUpload::make('avatar') + ->visible(fn () => config('panel.filament.avatar-provider') === 'local') + ->avatar() + ->directory('avatars') + ->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png'), ]), Tab::make(trans('profile.tabs.oauth')) diff --git a/app/Filament/Server/Resources/UserResource.php b/app/Filament/Server/Resources/UserResource.php index 660dc77b6..edfd2b83d 100644 --- a/app/Filament/Server/Resources/UserResource.php +++ b/app/Filament/Server/Resources/UserResource.php @@ -90,8 +90,8 @@ class UserResource extends Resource ImageColumn::make('picture') ->visibleFrom('lg') ->label('') - ->extraImgAttributes(['class' => 'rounded-full']) - ->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))), + ->alignCenter()->circular() + ->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)), TextColumn::make('username') ->searchable(), TextColumn::make('email') diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php index c57c6d25b..ab78204bd 100644 --- a/app/Models/ActivityLog.php +++ b/app/Models/ActivityLog.php @@ -6,6 +6,7 @@ use App\Traits\HasValidation; use Carbon\Carbon; use Illuminate\Support\Facades\Event; use App\Events\ActivityLogged; +use Filament\Facades\Filament; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; use Illuminate\Database\Eloquent\Builder; @@ -172,9 +173,11 @@ class ActivityLog extends Model implements HasIcon, HasLabel ]); } + $avatarUrl = Filament::getUserAvatarUrl($user); + return "
- +

$user->username — $this->event

diff --git a/app/Models/User.php b/app/Models/User.php index 3254526ba..8698bd2eb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,7 +9,6 @@ use App\Facades\Activity; use App\Traits\HasValidation; use DateTimeZone; use Filament\Models\Contracts\FilamentUser; -use Filament\Models\Contracts\HasAvatar; use Filament\Models\Contracts\HasName; use Filament\Models\Contracts\HasTenants; use Filament\Panel; @@ -50,7 +49,6 @@ use Spatie\Permission\Traits\HasRoles; * @property string|null $totp_secret * @property \Illuminate\Support\Carbon|null $totp_authenticated_at * @property string[]|null $oauth - * @property bool $gravatar * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Database\Eloquent\Collection|\App\Models\ApiKey[] $apiKeys @@ -77,7 +75,6 @@ use Spatie\Permission\Traits\HasRoles; * @method static Builder|User whereCreatedAt($value) * @method static Builder|User whereEmail($value) * @method static Builder|User whereExternalId($value) - * @method static Builder|User whereGravatar($value) * @method static Builder|User whereId($value) * @method static Builder|User whereLanguage($value) * @method static Builder|User whereTimezone($value) @@ -90,7 +87,7 @@ use Spatie\Permission\Traits\HasRoles; * @method static Builder|User whereUsername($value) * @method static Builder|User whereUuid($value) */ -class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName, HasTenants, Validatable +class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasName, HasTenants, Validatable { use Authenticatable; use Authorizable { can as protected canned; } @@ -124,7 +121,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'use_totp', 'totp_secret', 'totp_authenticated_at', - 'gravatar', 'oauth', 'customization', ]; @@ -169,7 +165,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac { return [ 'use_totp' => 'boolean', - 'gravatar' => 'boolean', 'totp_authenticated_at' => 'datetime', 'totp_secret' => 'encrypted', 'oauth' => 'array', @@ -377,11 +372,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return $this->username; } - public function getFilamentAvatarUrl(): ?string - { - return 'https://gravatar.com/avatar/' . md5(strtolower($this->email)); - } - public function canTarget(Model $user): bool { if ($this->isRootAdmin()) { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index acddc618e..fccfa461e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -10,6 +10,9 @@ use App\Checks\NodeVersionsCheck; use App\Checks\PanelVersionCheck; use App\Checks\ScheduleCheck; use App\Checks\UsedDiskSpaceCheck; +use App\Extensions\Avatar\Providers\GravatarProvider; +use App\Extensions\Avatar\Providers\LocalAvatarProvider; +use App\Extensions\Avatar\Providers\UiAvatarsProvider; use App\Extensions\OAuth\Providers\GitlabProvider; use App\Models; use App\Extensions\Captcha\Providers\TurnstileProvider; @@ -115,6 +118,11 @@ class AppServiceProvider extends ServiceProvider // Default Captcha provider TurnstileProvider::register($app); + // Default Avatar providers + GravatarProvider::register(); + UiAvatarsProvider::register(); + LocalAvatarProvider::register(); + FilamentColor::register([ 'danger' => Color::Red, 'gray' => Color::Zinc, diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 3cc811b9f..27f56f50c 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,6 +2,7 @@ namespace App\Providers\Filament; +use App\Extensions\Avatar\AvatarProvider; use App\Filament\Pages\Auth\Login; use App\Filament\Pages\Auth\EditProfile; use App\Http\Middleware\LanguageMiddleware; @@ -38,6 +39,7 @@ class AdminPanelProvider extends PanelProvider ->favicon(config('app.favicon', '/pelican.ico')) ->topNavigation(config('panel.filament.top-navigation', true)) ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) + ->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider')))) ->login(Login::class) ->passwordReset() ->userMenuItems([ diff --git a/app/Providers/Filament/AppPanelProvider.php b/app/Providers/Filament/AppPanelProvider.php index a56fd72bb..7f0288627 100644 --- a/app/Providers/Filament/AppPanelProvider.php +++ b/app/Providers/Filament/AppPanelProvider.php @@ -2,6 +2,7 @@ namespace App\Providers\Filament; +use App\Extensions\Avatar\AvatarProvider; use App\Filament\Pages\Auth\Login; use App\Filament\Pages\Auth\EditProfile; use Filament\Facades\Filament; @@ -34,6 +35,7 @@ class AppPanelProvider extends PanelProvider ->favicon(config('app.favicon', '/pelican.ico')) ->topNavigation(config('panel.filament.top-navigation', true)) ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) + ->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider')))) ->navigation(false) ->profile(EditProfile::class, false) ->login(Login::class) diff --git a/app/Providers/Filament/ServerPanelProvider.php b/app/Providers/Filament/ServerPanelProvider.php index 28e1f0a03..5e8ec8825 100644 --- a/app/Providers/Filament/ServerPanelProvider.php +++ b/app/Providers/Filament/ServerPanelProvider.php @@ -2,6 +2,7 @@ namespace App\Providers\Filament; +use App\Extensions\Avatar\AvatarProvider; use App\Filament\App\Resources\ServerResource\Pages\ListServers; use App\Filament\Pages\Auth\Login; use App\Filament\Admin\Resources\ServerResource\Pages\EditServer; @@ -41,6 +42,7 @@ class ServerPanelProvider extends PanelProvider ->favicon(config('app.favicon', '/pelican.ico')) ->topNavigation(config('panel.filament.top-navigation', true)) ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) + ->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider')))) ->login(Login::class) ->passwordReset() ->userMenuItems([ diff --git a/config/panel.php b/config/panel.php index b4ed78dbe..062ace41f 100644 --- a/config/panel.php +++ b/config/panel.php @@ -52,6 +52,7 @@ return [ 'filament' => [ 'top-navigation' => env('FILAMENT_TOP_NAVIGATION', false), 'display-width' => env('FILAMENT_WIDTH', 'screen-2xl'), + 'avatar-provider' => env('FILAMENT_AVATAR_PROVIDER', 'gravatar'), ], 'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true), diff --git a/database/migrations/2025_03_28_104348_drop_gravatar_column_in_users.php b/database/migrations/2025_03_28_104348_drop_gravatar_column_in_users.php new file mode 100644 index 000000000..bc352ba36 --- /dev/null +++ b/database/migrations/2025_03_28_104348_drop_gravatar_column_in_users.php @@ -0,0 +1,28 @@ +dropColumn('gravatar'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->boolean('gravatar')->default(true); + }); + } +}; diff --git a/lang/en/admin/setting.php b/lang/en/admin/setting.php index 729c9e265..a9ed2cf91 100644 --- a/lang/en/admin/setting.php +++ b/lang/en/admin/setting.php @@ -34,6 +34,7 @@ return [ 'clear' => 'Clear', 'set_to_cf' => 'Set to Cloudflare IPs', 'display_width' => 'Display Width', + 'avatar_provider' => 'Avatar Provider', ], 'captcha' => [ 'enable' => 'Enable',