diff --git a/app/Extensions/Avatar/AvatarProvider.php b/app/Extensions/Avatar/AvatarProvider.php deleted file mode 100644 index 978c95eba..000000000 --- a/app/Extensions/Avatar/AvatarProvider.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - 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; - - abstract public function get(User $user): ?string; - - public function getName(): string - { - return Str::title($this->getId()); - } -} diff --git a/app/Extensions/Avatar/AvatarSchemaInterface.php b/app/Extensions/Avatar/AvatarSchemaInterface.php new file mode 100644 index 000000000..fc1ab9b00 --- /dev/null +++ b/app/Extensions/Avatar/AvatarSchemaInterface.php @@ -0,0 +1,14 @@ +schemas, $id); + } + + public function getActiveSchema(): ?AvatarSchemaInterface + { + return $this->get($this->activeSchema); + } + + public function getAvatarUrl(User $user): ?string + { + if ($this->allowUploadedAvatars) { + $path = "avatars/$user->id.png"; + + if (Storage::disk('public')->exists($path)) { + return Storage::url($path); + } + } + + return $this->getActiveSchema()?->get($user); + } + + public function register(AvatarSchemaInterface $schema): void + { + if (array_key_exists($schema->getId(), $this->schemas)) { + return; + } + + $this->schemas[$schema->getId()] = $schema; + } + + /** @return array */ + public function getMappings(): array + { + return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all(); + } +} diff --git a/app/Extensions/Avatar/Providers/GravatarProvider.php b/app/Extensions/Avatar/Schemas/GravatarSchema.php similarity index 54% rename from app/Extensions/Avatar/Providers/GravatarProvider.php rename to app/Extensions/Avatar/Schemas/GravatarSchema.php index 21e69c587..953cab5a1 100644 --- a/app/Extensions/Avatar/Providers/GravatarProvider.php +++ b/app/Extensions/Avatar/Schemas/GravatarSchema.php @@ -1,24 +1,24 @@ email); } - - public static function register(): self - { - return new self(); - } } diff --git a/app/Extensions/Avatar/Providers/UiAvatarsProvider.php b/app/Extensions/Avatar/Schemas/UiAvatarsSchema.php similarity index 61% rename from app/Extensions/Avatar/Providers/UiAvatarsProvider.php rename to app/Extensions/Avatar/Schemas/UiAvatarsSchema.php index 4ee211cdb..2fe28191c 100644 --- a/app/Extensions/Avatar/Providers/UiAvatarsProvider.php +++ b/app/Extensions/Avatar/Schemas/UiAvatarsSchema.php @@ -1,11 +1,11 @@ */ + private array $schemas = []; + + /** + * @return CaptchaSchemaInterface[] + */ + public function getAll(): array + { + return $this->schemas; + } + + public function get(string $id): ?CaptchaSchemaInterface + { + return array_get($this->schemas, $id); + } + + public function register(CaptchaSchemaInterface $schema): void + { + if (array_key_exists($schema->getId(), $this->schemas)) { + return; + } + + config()->set('captcha.' . Str::lower($schema->getId()), $schema->getConfig()); + $this->schemas[$schema->getId()] = $schema; + } + + /** @return Collection */ + public function getActiveSchemas(): Collection + { + return collect($this->schemas) + ->filter(fn (CaptchaSchemaInterface $schema) => $schema->isEnabled()); + } + + public function getActiveSchema(): ?CaptchaSchemaInterface + { + return $this->getActiveSchemas()->first(); + } +} diff --git a/app/Extensions/Captcha/Providers/CaptchaProvider.php b/app/Extensions/Captcha/Schemas/BaseSchema.php similarity index 51% rename from app/Extensions/Captcha/Providers/CaptchaProvider.php rename to app/Extensions/Captcha/Schemas/BaseSchema.php index 7c5db3009..8d7d028ea 100644 --- a/app/Extensions/Captcha/Providers/CaptchaProvider.php +++ b/app/Extensions/Captcha/Schemas/BaseSchema.php @@ -1,45 +1,19 @@ - */ - protected static array $providers = []; - - /** - * @return self|static[] - */ - public static function get(?string $id = null): array|self - { - return $id ? static::$providers[$id] : static::$providers; - } - - protected function __construct(protected Application $app) - { - if (array_key_exists($this->getId(), static::$providers)) { - if (!$this->app->runningUnitTests()) { - logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'"); - } - - return; - } - - config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig()); - - static::$providers[$this->getId()] = $this; - } - abstract public function getId(): string; - abstract public function getComponent(): Component; + public function getName(): string + { + return Str::upper($this->getId()); + } /** * @return array @@ -83,34 +57,6 @@ abstract class CaptchaProvider ]; } - public function getName(): string - { - return Str::title($this->getId()); - } - - public function getIcon(): ?string - { - return null; - } - - public function isEnabled(): bool - { - $id = Str::upper($this->getId()); - - return env("CAPTCHA_{$id}_ENABLED", false); - } - - /** - * @return array - */ - public function validateResponse(?string $captchaResponse = null): array - { - return [ - 'success' => false, - 'message' => 'validateResponse not defined', - ]; - } - public function verifyDomain(string $hostname, ?string $requestUrl = null): bool { return true; diff --git a/app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php b/app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php new file mode 100644 index 000000000..908ecaa08 --- /dev/null +++ b/app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php @@ -0,0 +1,35 @@ + + */ + public function getConfig(): array; + + public function isEnabled(): bool; + + public function getFormComponent(): Component; + + /** + * @return Component[] + */ + public function getSettingsForm(): array; + + public function getIcon(): ?string; + + /** + * @return array + */ + public function validateResponse(?string $captchaResponse = null): array; + + public function verifyDomain(string $hostname, ?string $requestUrl = null): bool; +} diff --git a/app/Filament/Components/Forms/Fields/TurnstileCaptcha.php b/app/Extensions/Captcha/Schemas/Turnstile/Component.php similarity index 54% rename from app/Filament/Components/Forms/Fields/TurnstileCaptcha.php rename to app/Extensions/Captcha/Schemas/Turnstile/Component.php index 694b34f00..107e134cf 100644 --- a/app/Filament/Components/Forms/Fields/TurnstileCaptcha.php +++ b/app/Extensions/Captcha/Schemas/Turnstile/Component.php @@ -1,11 +1,10 @@ required(); - $this->after(function (TurnstileCaptcha $component) { - $component->rule(new ValidTurnstileCaptcha()); - }); + $this->rule(new Rule()); } } diff --git a/app/Rules/ValidTurnstileCaptcha.php b/app/Extensions/Captcha/Schemas/Turnstile/Rule.php similarity index 51% rename from app/Rules/ValidTurnstileCaptcha.php rename to app/Extensions/Captcha/Schemas/Turnstile/Rule.php index 83783e063..05775fd59 100644 --- a/app/Rules/ValidTurnstileCaptcha.php +++ b/app/Extensions/Captcha/Schemas/Turnstile/Rule.php @@ -1,16 +1,17 @@ validateResponse($value); + $response = App::call(fn (CaptchaService $service) => $service->getActiveSchema()->validateResponse($value)); if (!$response['success']) { $fail($response['message'] ?? 'Unknown error occurred, please try again'); diff --git a/app/Extensions/Captcha/Providers/TurnstileProvider.php b/app/Extensions/Captcha/Schemas/Turnstile/TurnstileSchema.php similarity index 82% rename from app/Extensions/Captcha/Providers/TurnstileProvider.php rename to app/Extensions/Captcha/Schemas/Turnstile/TurnstileSchema.php index 70980a249..07b0280e9 100644 --- a/app/Extensions/Captcha/Providers/TurnstileProvider.php +++ b/app/Extensions/Captcha/Schemas/Turnstile/TurnstileSchema.php @@ -1,26 +1,31 @@ label(trans('admin/setting.captcha.info_label')) ->columnSpan(2) ->content(new HtmlString(trans('admin/setting.captcha.info'))), - ]); } - public function getIcon(): string + public function getIcon(): ?string { return 'tabler-brand-cloudflare'; } - public static function register(Application $app): self - { - return new self($app); - } - /** * @return array */ diff --git a/app/Extensions/Features/FeatureProvider.php b/app/Extensions/Features/FeatureProvider.php deleted file mode 100644 index 2558bd1ed..000000000 --- a/app/Extensions/Features/FeatureProvider.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - protected static array $providers = []; - - /** - * @param string[] $id - * @return self|static[] - */ - public static function getProviders(string|array|null $id = null): array|self - { - if (is_array($id)) { - return array_intersect_key(static::$providers, array_flip($id)); - } - - return $id ? static::$providers[$id] : static::$providers; - } - - protected function __construct(protected Application $app) - { - if (array_key_exists($this->getId(), static::$providers)) { - if (!$this->app->runningUnitTests()) { - logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'"); - } - - return; - } - - static::$providers[$this->getId()] = $this; - } - - abstract public function getId(): string; - - /** - * A matching subset string (case-insensitive) from the console output - * - * @return array - */ - abstract public function getListeners(): array; - - abstract public function getAction(): Action; -} diff --git a/app/Extensions/Features/FeatureSchemaInterface.php b/app/Extensions/Features/FeatureSchemaInterface.php new file mode 100644 index 000000000..e0a35ded0 --- /dev/null +++ b/app/Extensions/Features/FeatureSchemaInterface.php @@ -0,0 +1,15 @@ +schemas; + } + + public function get(string $id): ?FeatureSchemaInterface + { + return array_get($this->schemas, $id); + } + + /** + * @param string[] $features + * @return FeatureSchemaInterface[] + */ + public function getActiveSchemas(array $features): array + { + return collect($this->schemas)->only($features)->all(); + } + + public function register(FeatureSchemaInterface $schema): void + { + if (array_key_exists($schema->getId(), $this->schemas)) { + return; + } + + $this->schemas[$schema->getId()] = $schema; + } + + /** + * @param string[] $features + * @return array> + */ + public function getMappings(array $features): array + { + return collect($this->getActiveSchemas($features)) + ->mapWithKeys(fn (FeatureSchemaInterface $schema) => [ + $schema->getId() => $schema->getListeners(), + ])->all(); + } +} diff --git a/app/Extensions/Features/GSLToken.php b/app/Extensions/Features/Schemas/GSLTokenSchema.php similarity index 93% rename from app/Extensions/Features/GSLToken.php rename to app/Extensions/Features/Schemas/GSLTokenSchema.php index 7f6f0ff83..698d3bcdc 100644 --- a/app/Extensions/Features/GSLToken.php +++ b/app/Extensions/Features/Schemas/GSLTokenSchema.php @@ -1,7 +1,8 @@ */ public function getListeners(): array { @@ -119,9 +114,4 @@ class GSLToken extends FeatureProvider } }); } - - public static function register(Application $app): self - { - return new self($app); - } } diff --git a/app/Extensions/Features/JavaVersion.php b/app/Extensions/Features/Schemas/JavaVersionSchema.php similarity index 91% rename from app/Extensions/Features/JavaVersion.php rename to app/Extensions/Features/Schemas/JavaVersionSchema.php index 10701bb97..6b79ff28d 100644 --- a/app/Extensions/Features/JavaVersion.php +++ b/app/Extensions/Features/Schemas/JavaVersionSchema.php @@ -1,7 +1,8 @@ */ public function getListeners(): array { @@ -92,9 +87,4 @@ class JavaVersion extends FeatureProvider } }); } - - public static function register(Application $app): self - { - return new self($app); - } } diff --git a/app/Extensions/Features/MinecraftEula.php b/app/Extensions/Features/Schemas/MinecraftEulaSchema.php similarity index 85% rename from app/Extensions/Features/MinecraftEula.php rename to app/Extensions/Features/Schemas/MinecraftEulaSchema.php index 9b4ccccd1..9249748d6 100644 --- a/app/Extensions/Features/MinecraftEula.php +++ b/app/Extensions/Features/Schemas/MinecraftEulaSchema.php @@ -1,7 +1,8 @@ */ public function getListeners(): array { @@ -63,9 +58,4 @@ class MinecraftEula extends FeatureProvider } }); } - - public static function register(Application $app): self - { - return new self($app); - } } diff --git a/app/Extensions/Features/PIDLimit.php b/app/Extensions/Features/Schemas/PIDLimitSchema.php similarity index 87% rename from app/Extensions/Features/PIDLimit.php rename to app/Extensions/Features/Schemas/PIDLimitSchema.php index dfae1d8c4..29b4abb6f 100644 --- a/app/Extensions/Features/PIDLimit.php +++ b/app/Extensions/Features/Schemas/PIDLimitSchema.php @@ -1,19 +1,14 @@ */ public function getListeners(): array { @@ -68,9 +63,4 @@ class PIDLimit extends FeatureProvider ->modalCancelActionLabel('Close') ->action(fn () => null); } - - public static function register(Application $app): self - { - return new self($app); - } } diff --git a/app/Extensions/Features/SteamDiskSpace.php b/app/Extensions/Features/Schemas/SteamDiskSpaceSchema.php similarity index 83% rename from app/Extensions/Features/SteamDiskSpace.php rename to app/Extensions/Features/Schemas/SteamDiskSpaceSchema.php index c4479a987..b6ad64a12 100644 --- a/app/Extensions/Features/SteamDiskSpace.php +++ b/app/Extensions/Features/Schemas/SteamDiskSpaceSchema.php @@ -1,19 +1,14 @@ */ public function getListeners(): array { @@ -56,9 +51,4 @@ class SteamDiskSpace extends FeatureProvider ->modalCancelActionLabel('Close') ->action(fn () => null); } - - public static function register(Application $app): self - { - return new self($app); - } } diff --git a/app/Extensions/OAuth/OAuthSchemaInterface.php b/app/Extensions/OAuth/OAuthSchemaInterface.php new file mode 100644 index 000000000..837705888 --- /dev/null +++ b/app/Extensions/OAuth/OAuthSchemaInterface.php @@ -0,0 +1,35 @@ + + */ + public function getServiceConfig(): array; + + /** @return Component[] */ + public function getSettingsForm(): array; + + /** @return Step[] */ + public function getSetupSteps(): array; + + public function getIcon(): ?string; + + public function getHexColor(): ?string; + + public function isEnabled(): bool; +} diff --git a/app/Extensions/OAuth/OAuthService.php b/app/Extensions/OAuth/OAuthService.php new file mode 100644 index 000000000..cedd4dc1c --- /dev/null +++ b/app/Extensions/OAuth/OAuthService.php @@ -0,0 +1,46 @@ +schemas; + } + + public function get(string $id): ?OAuthSchemaInterface + { + return array_get($this->schemas, $id); + } + + /** @return OAuthSchemaInterface[] */ + public function getEnabled(): array + { + return collect($this->schemas) + ->filter(fn (OAuthSchemaInterface $schema) => $schema->isEnabled()) + ->all(); + } + + public function register(OAuthSchemaInterface $schema): void + { + if (array_key_exists($schema->getId(), $this->schemas)) { + return; + } + + config()->set('services.' . $schema->getId(), array_merge($schema->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $schema->getId()])); + + if ($schema->getSocialiteProvider()) { + Event::listen(fn (SocialiteWasCalled $event) => $event->extendSocialite($schema->getId(), $schema->getSocialiteProvider())); + } + + $this->schemas[$schema->getId()] = $schema; + } +} diff --git a/app/Extensions/OAuth/Providers/CommonProvider.php b/app/Extensions/OAuth/Providers/CommonProvider.php deleted file mode 100644 index db48ff03c..000000000 --- a/app/Extensions/OAuth/Providers/CommonProvider.php +++ /dev/null @@ -1,38 +0,0 @@ -id; - } - - public function getProviderClass(): ?string - { - return $this->providerClass; - } - - public function getIcon(): ?string - { - return $this->icon; - } - - public function getHexColor(): ?string - { - return $this->hexColor; - } - - public static function register(Application $app, string $id, ?string $providerClass = null, ?string $icon = null, ?string $hexColor = null): static - { - return new self($app, $id, $providerClass, $icon, $hexColor); - } -} diff --git a/app/Extensions/OAuth/Providers/AuthentikProvider.php b/app/Extensions/OAuth/Schemas/AuthentikSchema.php similarity index 81% rename from app/Extensions/OAuth/Providers/AuthentikProvider.php rename to app/Extensions/OAuth/Schemas/AuthentikSchema.php index f6be54a99..10e9be348 100644 --- a/app/Extensions/OAuth/Providers/AuthentikProvider.php +++ b/app/Extensions/OAuth/Schemas/AuthentikSchema.php @@ -1,25 +1,19 @@ id; + } + + public function getName(): string + { + return $this->name ?? parent::getName(); + } + + public function getConfigKey(): string + { + return $this->configName ?? parent::getConfigKey(); + } + + public function getIcon(): ?string + { + return $this->icon; + } + + public function getHexColor(): ?string + { + return $this->hexColor; + } +} diff --git a/app/Extensions/OAuth/Providers/DiscordProvider.php b/app/Extensions/OAuth/Schemas/DiscordSchema.php similarity index 82% rename from app/Extensions/OAuth/Providers/DiscordProvider.php rename to app/Extensions/OAuth/Schemas/DiscordSchema.php index 5981172f9..39bb33bd9 100644 --- a/app/Extensions/OAuth/Providers/DiscordProvider.php +++ b/app/Extensions/OAuth/Schemas/DiscordSchema.php @@ -1,29 +1,23 @@ - */ - protected static array $providers = []; - - /** - * @return self|static[] - */ - public static function get(?string $id = null): array|self - { - return $id ? static::$providers[$id] : static::$providers; - } - - protected function __construct(protected Application $app) - { - if (array_key_exists($this->getId(), static::$providers)) { - if (!$this->app->runningUnitTests()) { - logger()->warning("Tried to create duplicate OAuth provider with id '{$this->getId()}'"); - } - - return; - } - - config()->set('services.' . $this->getId(), array_merge($this->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $this->getId()])); - - if ($this->getProviderClass()) { - Event::listen(function (SocialiteWasCalled $event) { - $event->extendSocialite($this->getId(), $this->getProviderClass()); - }); - } - - static::$providers[$this->getId()] = $this; - } - abstract public function getId(): string; - public function getProviderClass(): ?string + public function getSocialiteProvider(): ?string { return null; } - /** - * @return array - */ public function getServiceConfig(): array { $id = Str::upper($this->getId()); @@ -112,6 +73,13 @@ abstract class OAuthProvider return Str::title($this->getId()); } + public function getConfigKey(): string + { + $id = Str::upper($this->getId()); + + return "OAUTH_{$id}_ENABLED"; + } + public function getIcon(): ?string { return null; diff --git a/app/Extensions/OAuth/Providers/SteamProvider.php b/app/Extensions/OAuth/Schemas/SteamSchema.php similarity index 82% rename from app/Extensions/OAuth/Providers/SteamProvider.php rename to app/Extensions/OAuth/Schemas/SteamSchema.php index 85e61ee1b..1c29f01f6 100644 --- a/app/Extensions/OAuth/Providers/SteamProvider.php +++ b/app/Extensions/OAuth/Schemas/SteamSchema.php @@ -1,28 +1,22 @@ |null */ public ?array $data = []; @@ -66,6 +72,13 @@ class Settings extends Page implements HasForms $this->form->fill(); } + public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService): void + { + $this->oauthService = $oauthService; + $this->avatarService = $avatarService; + $this->captchaService = $captchaService; + } + public static function canAccess(): bool { return auth()->user()->can('view settings'); @@ -173,7 +186,7 @@ class Settings extends Page implements HasForms Select::make('FILAMENT_AVATAR_PROVIDER') ->label(trans('admin/setting.general.avatar_provider')) ->native(false) - ->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()])) + ->options($this->avatarService->getMappings()) ->selectablePlaceholder(false) ->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))), Toggle::make('FILAMENT_UPLOADABLE_AVATARS') @@ -264,15 +277,14 @@ class Settings extends Page implements HasForms { $formFields = []; - $captchaProviders = CaptchaProvider::get(); - foreach ($captchaProviders as $captchaProvider) { - $id = Str::upper($captchaProvider->getId()); - $name = Str::title($captchaProvider->getId()); + $captchaSchemas = $this->captchaService->getAll(); + foreach ($captchaSchemas as $schema) { + $id = Str::upper($schema->getId()); - $formFields[] = Section::make($name) + $formFields[] = Section::make($schema->getName()) ->columns(5) - ->icon($captchaProvider->getIcon() ?? 'tabler-shield') - ->collapsed(fn () => !env("CAPTCHA_{$id}_ENABLED", false)) + ->icon($schema->getIcon() ?? 'tabler-shield') + ->collapsed(fn () => !$schema->isEnabled()) ->collapsible() ->schema([ Hidden::make("CAPTCHA_{$id}_ENABLED") @@ -283,21 +295,14 @@ class Settings extends Page implements HasForms ->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED")) ->label(trans('admin/setting.captcha.disable')) ->color('danger') - ->action(function (Set $set) use ($id) { - $set("CAPTCHA_{$id}_ENABLED", false); - }), + ->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", false)), FormAction::make("enable_captcha_$id") ->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED")) ->label(trans('admin/setting.captcha.enable')) ->color('success') - ->action(function (Set $set) use ($id, $captchaProviders) { - foreach ($captchaProviders as $captchaProvider) { - $loopId = Str::upper($captchaProvider->getId()); - $set("CAPTCHA_{$loopId}_ENABLED", $loopId === $id); - } - }), + ->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", true)), ])->columnSpan(1), - Group::make($captchaProvider->getSettingsForm()) + Group::make($schema->getSettingsForm()) ->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED")) ->columns(4) ->columnSpan(4), @@ -533,39 +538,37 @@ class Settings extends Page implements HasForms { $formFields = []; - $oauthProviders = OAuthProvider::get(); - foreach ($oauthProviders as $oauthProvider) { - $id = Str::upper($oauthProvider->getId()); - $name = Str::title($oauthProvider->getId()); + $oauthSchemas = $this->oauthService->getAll(); + foreach ($oauthSchemas as $schema) { + $id = Str::upper($schema->getId()); + $key = $schema->getConfigKey(); - $formFields[] = Section::make($name) + $formFields[] = Section::make($schema->getName()) ->columns(5) - ->icon($oauthProvider->getIcon() ?? 'tabler-brand-oauth') - ->collapsed(fn () => !env("OAUTH_{$id}_ENABLED", false)) + ->icon($schema->getIcon() ?? 'tabler-brand-oauth') + ->collapsed(fn () => !env($key, false)) ->collapsible() ->schema([ - Hidden::make("OAUTH_{$id}_ENABLED") + Hidden::make($key) ->live() - ->default(env("OAUTH_{$id}_ENABLED")), + ->default(env($key)), Actions::make([ FormAction::make("disable_oauth_$id") - ->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED")) + ->visible(fn (Get $get) => $get($key)) ->label(trans('admin/setting.oauth.disable')) ->color('danger') - ->action(function (Set $set) use ($id) { - $set("OAUTH_{$id}_ENABLED", false); - }), + ->action(fn (Set $set) => $set($key, false)), FormAction::make("enable_oauth_$id") - ->visible(fn (Get $get) => !$get("OAUTH_{$id}_ENABLED")) + ->visible(fn (Get $get) => !$get($key)) ->label(trans('admin/setting.oauth.enable')) ->color('success') - ->steps($oauthProvider->getSetupSteps()) - ->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $name) + ->steps($schema->getSetupSteps()) + ->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $schema->getName()) ->modalSubmitActionLabel(trans('admin/setting.oauth.enable')) ->modalCancelAction(false) - ->action(function ($data, Set $set) use ($id) { + ->action(function ($data, Set $set) use ($key) { $data = array_merge([ - "OAUTH_{$id}_ENABLED" => 'true', + $key => 'true', ], $data); foreach ($data as $key => $value) { @@ -573,8 +576,8 @@ class Settings extends Page implements HasForms } }), ])->columnSpan(1), - Group::make($oauthProvider->getSettingsForm()) - ->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED")) + Group::make($schema->getSettingsForm()) + ->visible(fn (Get $get) => $get($key)) ->columns(4) ->columnSpan(4), ]); diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index 4f7b05df7..b546c0e3e 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -3,7 +3,7 @@ namespace App\Filament\Pages\Auth; use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; -use App\Extensions\OAuth\Providers\OAuthProvider; +use App\Extensions\OAuth\OAuthService; use App\Facades\Activity; use App\Models\ActivityLog; use App\Models\ApiKey; @@ -60,9 +60,12 @@ class EditProfile extends BaseEditProfile private ToggleTwoFactorService $toggleTwoFactorService; - public function boot(ToggleTwoFactorService $toggleTwoFactorService): void + protected OAuthService $oauthService; + + public function boot(ToggleTwoFactorService $toggleTwoFactorService, OAuthService $oauthService): void { $this->toggleTwoFactorService = $toggleTwoFactorService; + $this->oauthService = $oauthService; } public function getMaxWidth(): MaxWidth|string @@ -72,7 +75,7 @@ class EditProfile extends BaseEditProfile protected function getForms(): array { - $oauthProviders = collect(OAuthProvider::get())->filter(fn (OAuthProvider $provider) => $provider->isEnabled())->all(); + $oauthSchemas = $this->oauthService->getEnabled(); return [ 'form' => $this->form( @@ -155,21 +158,21 @@ class EditProfile extends BaseEditProfile Tab::make(trans('profile.tabs.oauth')) ->icon('tabler-brand-oauth') - ->visible(count($oauthProviders) > 0) - ->schema(function () use ($oauthProviders) { + ->visible(count($oauthSchemas) > 0) + ->schema(function () use ($oauthSchemas) { $actions = []; - foreach ($oauthProviders as $oauthProvider) { + foreach ($oauthSchemas as $schema) { - $id = $oauthProvider->getId(); - $name = $oauthProvider->getName(); + $id = $schema->getId(); + $name = $schema->getName(); $unlink = array_key_exists($id, $this->getUser()->oauth ?? []); $actions[] = Action::make("oauth_$id") ->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name) ->icon($unlink ? 'tabler-unlink' : 'tabler-link') - ->color(Color::hex($oauthProvider->getHexColor())) + ->color(Color::hex($schema->getHexColor())) ->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) { if ($unlink) { $oauth = auth()->user()->oauth; diff --git a/app/Filament/Pages/Auth/Login.php b/app/Filament/Pages/Auth/Login.php index 24105168f..443203a01 100644 --- a/app/Filament/Pages/Auth/Login.php +++ b/app/Filament/Pages/Auth/Login.php @@ -3,8 +3,8 @@ namespace App\Filament\Pages\Auth; use App\Events\Auth\ProvidedAuthenticationToken; -use App\Extensions\Captcha\Providers\CaptchaProvider; -use App\Extensions\OAuth\Providers\OAuthProvider; +use App\Extensions\Captcha\CaptchaService; +use App\Extensions\OAuth\OAuthService; use App\Facades\Activity; use App\Models\User; use Filament\Facades\Filament; @@ -27,9 +27,15 @@ class Login extends BaseLogin public bool $verifyTwoFactor = false; - public function boot(Google2FA $google2FA): void + protected OAuthService $oauthService; + + protected CaptchaService $captchaService; + + public function boot(Google2FA $google2FA, OAuthService $oauthService, CaptchaService $captchaService): void { $this->google2FA = $google2FA; + $this->oauthService = $oauthService; + $this->captchaService = $captchaService; } public function authenticate(): ?LoginResponse @@ -116,8 +122,8 @@ class Login extends BaseLogin $this->getTwoFactorAuthenticationComponent(), ]; - if ($captchaProvider = $this->getCaptchaComponent()) { - $schema = array_merge($schema, [$captchaProvider]); + if ($captchaComponent = $this->getCaptchaComponent()) { + $schema = array_merge($schema, [$captchaComponent]); } return [ @@ -142,13 +148,7 @@ class Login extends BaseLogin private function getCaptchaComponent(): ?Component { - $captchaProvider = collect(CaptchaProvider::get())->filter(fn (CaptchaProvider $provider) => $provider->isEnabled())->first(); - - if (!$captchaProvider) { - return null; - } - - return $captchaProvider->getComponent(); + return $this->captchaService->getActiveSchema()?->getFormComponent(); } protected function throwFailureValidationException(): never @@ -174,16 +174,16 @@ class Login extends BaseLogin { $actions = []; - $oauthProviders = collect(OAuthProvider::get())->filter(fn (OAuthProvider $provider) => $provider->isEnabled())->all(); + $oauthSchemas = $this->oauthService->getEnabled(); - foreach ($oauthProviders as $oauthProvider) { + foreach ($oauthSchemas as $schema) { - $id = $oauthProvider->getId(); + $id = $schema->getId(); $actions[] = Action::make("oauth_$id") - ->label($oauthProvider->getName()) - ->icon($oauthProvider->getIcon()) - ->color(Color::hex($oauthProvider->getHexColor())) + ->label($schema->getName()) + ->icon($schema->getIcon()) + ->color(Color::hex($schema->getHexColor())) ->url(route('auth.oauth.redirect', ['driver' => $id], false)); } diff --git a/app/Filament/Server/Pages/Console.php b/app/Filament/Server/Pages/Console.php index 42ae850be..dc8488ee4 100644 --- a/app/Filament/Server/Pages/Console.php +++ b/app/Filament/Server/Pages/Console.php @@ -5,7 +5,7 @@ namespace App\Filament\Server\Pages; use App\Enums\ConsoleWidgetPosition; use App\Enums\ContainerStatus; use App\Exceptions\Http\Server\ServerStateConflictException; -use App\Extensions\Features\FeatureProvider; +use App\Extensions\Features\FeatureService; use App\Filament\Server\Widgets\ServerConsole; use App\Filament\Server\Widgets\ServerCpuChart; use App\Filament\Server\Widgets\ServerMemoryChart; @@ -38,6 +38,8 @@ class Console extends Page public ContainerStatus $status = ContainerStatus::Offline; + protected FeatureService $featureService; + public function mount(): void { /** @var Server $server */ @@ -54,12 +56,12 @@ class Console extends Page } } - public function boot(): void + public function boot(FeatureService $featureService): void { + $this->featureService = $featureService; /** @var Server $server */ $server = Filament::getTenant(); - /** @var FeatureProvider $feature */ - foreach ($server->egg->features() as $feature) { + foreach ($featureService->getActiveSchemas($server->egg->features) as $feature) { $this->cacheAction($feature->getAction()); } } @@ -70,8 +72,8 @@ class Console extends Page $data = json_decode($data); $feature = data_get($data, 'key'); - $feature = FeatureProvider::getProviders($feature); - if ($this->getMountedAction()) { + $feature = $this->featureService->get($feature); + if (!$feature || $this->getMountedAction()) { return; } $this->mountAction($feature->getId()); diff --git a/app/Http/Controllers/Auth/OAuthController.php b/app/Http/Controllers/Auth/OAuthController.php index 313bca976..97e6a4f15 100644 --- a/app/Http/Controllers/Auth/OAuthController.php +++ b/app/Http/Controllers/Auth/OAuthController.php @@ -2,23 +2,24 @@ namespace App\Http\Controllers\Auth; -use App\Extensions\OAuth\Providers\OAuthProvider; +use App\Extensions\OAuth\OAuthService; use App\Filament\Pages\Auth\EditProfile; -use Filament\Notifications\Notification; -use Illuminate\Auth\AuthManager; -use Illuminate\Http\RedirectResponse; -use Laravel\Socialite\Facades\Socialite; use App\Http\Controllers\Controller; use App\Models\User; use App\Services\Users\UserUpdateService; use Exception; +use Filament\Notifications\Notification; +use Illuminate\Auth\AuthManager; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Laravel\Socialite\Facades\Socialite; class OAuthController extends Controller { public function __construct( private readonly AuthManager $auth, - private readonly UserUpdateService $updateService + private readonly UserUpdateService $updateService, + private readonly OAuthService $oauthService ) {} /** @@ -27,7 +28,7 @@ class OAuthController extends Controller public function redirect(string $driver): RedirectResponse { // Driver is disabled - redirect to normal login - if (!OAuthProvider::get($driver)->isEnabled()) { + if (!$this->oauthService->get($driver)->isEnabled()) { return redirect()->route('auth.login'); } @@ -40,7 +41,7 @@ class OAuthController extends Controller public function callback(Request $request, string $driver): RedirectResponse { // Driver is disabled - redirect to normal login - if (!OAuthProvider::get($driver)->isEnabled()) { + if (!$this->oauthService->get($driver)?->isEnabled()) { return redirect()->route('auth.login'); } diff --git a/app/Http/Middleware/VerifyCaptcha.php b/app/Http/Middleware/VerifyCaptcha.php index 5836682b7..4e06b10fb 100644 --- a/app/Http/Middleware/VerifyCaptcha.php +++ b/app/Http/Middleware/VerifyCaptcha.php @@ -2,35 +2,35 @@ namespace App\Http\Middleware; +use App\Extensions\Captcha\CaptchaService; use Closure; use Illuminate\Foundation\Application; use Illuminate\Http\Request; use Illuminate\Http\Response; use App\Events\Auth\FailedCaptcha; -use App\Extensions\Captcha\Providers\CaptchaProvider; use Symfony\Component\HttpKernel\Exception\HttpException; readonly class VerifyCaptcha { public function __construct(private Application $app) {} - public function handle(Request $request, Closure $next): mixed + public function handle(Request $request, Closure $next, CaptchaService $captchaService): mixed { if ($this->app->isLocal()) { return $next($request); } - $captchaProviders = collect(CaptchaProvider::get())->filter(fn (CaptchaProvider $provider) => $provider->isEnabled())->all(); - foreach ($captchaProviders as $captchaProvider) { - $response = $captchaProvider->validateResponse(); + $schemas = $captchaService->getActiveSchemas(); + foreach ($schemas as $schema) { + $response = $schema->validateResponse(); - if ($response['success'] && $captchaProvider->verifyDomain($response['hostname'] ?? '', $request->url())) { + if ($response['success'] && $schema->verifyDomain($response['hostname'] ?? '', $request->url())) { return $next($request); } event(new FailedCaptcha($request->ip(), $response['message'] ?? null)); - throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$captchaProvider->getId()} captcha data."); + throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$schema->getId()} captcha data."); } // No captcha enabled diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 54883e4ba..a68d21311 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -5,13 +5,12 @@ namespace App\Models; use App\Contracts\Validatable; use App\Exceptions\Service\Egg\HasChildrenException; use App\Exceptions\Service\HasActiveServersException; -use App\Extensions\Features\FeatureProvider; use App\Traits\HasValidation; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Str; /** @@ -161,12 +160,6 @@ class Egg extends Model implements Validatable }); } - /** @return array */ - public function features(): array - { - return FeatureProvider::getProviders($this->features); - } - /** * Returns the install script for the egg; if egg is copying from another * it will return the copied script. diff --git a/app/Models/User.php b/app/Models/User.php index a5bdb8305..157f856f5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,9 +4,8 @@ namespace App\Models; use App\Contracts\Validatable; use App\Exceptions\DisplayException; -use App\Extensions\Avatar\AvatarProvider; +use App\Extensions\Avatar\AvatarService; use App\Rules\Username; -use App\Facades\Activity; use App\Traits\HasValidation; use DateTimeZone; use Filament\Models\Contracts\FilamentUser; @@ -18,6 +17,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\App; use Illuminate\Support\Str; use Illuminate\Validation\Rules\In; use Illuminate\Auth\Authenticatable; @@ -32,7 +32,6 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; -use Illuminate\Support\Facades\Storage; use ResourceBundle; use Spatie\Permission\Traits\HasRoles; @@ -397,17 +396,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac public function getFilamentAvatarUrl(): ?string { - if (config('panel.filament.uploadable-avatars')) { - $path = "avatars/$this->id.png"; - - if (Storage::disk('public')->exists($path)) { - return Storage::url($path); - } - } - - $provider = AvatarProvider::getProvider(config('panel.filament.avatar-provider')); - - return $provider?->get($this); + return App::call(fn (AvatarService $service) => $service->getAvatarUrl($this)); } public function canTarget(Model $model): bool diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4c3e7ea34..16c2f48ef 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -10,21 +10,7 @@ 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\UiAvatarsProvider; -use App\Extensions\OAuth\Providers\GitlabProvider; use App\Models; -use App\Extensions\Captcha\Providers\TurnstileProvider; -use App\Extensions\Features\GSLToken; -use App\Extensions\Features\JavaVersion; -use App\Extensions\Features\MinecraftEula; -use App\Extensions\Features\PIDLimit; -use App\Extensions\Features\SteamDiskSpace; -use App\Extensions\OAuth\Providers\AuthentikProvider; -use App\Extensions\OAuth\Providers\CommonProvider; -use App\Extensions\OAuth\Providers\DiscordProvider; -use App\Extensions\OAuth\Providers\GithubProvider; -use App\Extensions\OAuth\Providers\SteamProvider; use App\Services\Helpers\SoftwareVersionService; use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; @@ -105,35 +91,6 @@ class AppServiceProvider extends ServiceProvider Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens); Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens); - // Default OAuth providers included with Socialite - CommonProvider::register($app, 'facebook', null, 'tabler-brand-facebook-f', '#1877f2'); - CommonProvider::register($app, 'x', null, 'tabler-brand-x-f', '#1da1f2'); - CommonProvider::register($app, 'linkedin', null, 'tabler-brand-linkedin-f', '#0a66c2'); - CommonProvider::register($app, 'google', null, 'tabler-brand-google-f', '#4285f4'); - GithubProvider::register($app); - GitlabProvider::register($app); - CommonProvider::register($app, 'bitbucket', null, 'tabler-brand-bitbucket-f', '#205081'); - CommonProvider::register($app, 'slack', null, 'tabler-brand-slack', '#6ecadc'); - - // Additional OAuth providers from socialiteproviders.com - AuthentikProvider::register($app); - DiscordProvider::register($app); - SteamProvider::register($app); - - // Default Captcha provider - TurnstileProvider::register($app); - - // Default Avatar providers - GravatarProvider::register(); - UiAvatarsProvider::register(); - - // Default Feature providers - GSLToken::register($app); - JavaVersion::register($app); - MinecraftEula::register($app); - PIDLimit::register($app); - SteamDiskSpace::register($app); - FilamentColor::register([ 'danger' => Color::Red, 'gray' => Color::Zinc, diff --git a/app/Providers/Extensions/AvatarServiceProvider.php b/app/Providers/Extensions/AvatarServiceProvider.php new file mode 100644 index 000000000..a108c690d --- /dev/null +++ b/app/Providers/Extensions/AvatarServiceProvider.php @@ -0,0 +1,24 @@ +app->singleton(AvatarService::class, function ($app) { + $service = new AvatarService(config('panel.filament.uploadable-avatars', false), config('panel.filament.avatar-provider', 'gravatar')); + + // Default Avatar providers + $service->register(new GravatarSchema()); + $service->register(new UiAvatarsSchema()); + + return $service; + }); + } +} diff --git a/app/Providers/Extensions/CaptchaServiceProvider.php b/app/Providers/Extensions/CaptchaServiceProvider.php new file mode 100644 index 000000000..82c891bd9 --- /dev/null +++ b/app/Providers/Extensions/CaptchaServiceProvider.php @@ -0,0 +1,22 @@ +app->singleton(CaptchaService::class, function ($app) { + $service = new CaptchaService(); + + // Default Captcha providers + $service->register(new TurnstileSchema()); + + return $service; + }); + } +} diff --git a/app/Providers/Extensions/FeatureServiceProvider.php b/app/Providers/Extensions/FeatureServiceProvider.php new file mode 100644 index 000000000..abdce5524 --- /dev/null +++ b/app/Providers/Extensions/FeatureServiceProvider.php @@ -0,0 +1,30 @@ +app->singleton(FeatureService::class, function ($app) { + $provider = new FeatureService(); + + // Default Feature providers + $provider->register(new GSLTokenSchema()); + $provider->register(new JavaVersionSchema()); + $provider->register(new MinecraftEulaSchema()); + $provider->register(new PIDLimitSchema()); + $provider->register(new SteamDiskSpaceSchema()); + + return $provider; + }); + } +} diff --git a/app/Providers/Extensions/OAuthServiceProvider.php b/app/Providers/Extensions/OAuthServiceProvider.php new file mode 100644 index 000000000..6d64ce496 --- /dev/null +++ b/app/Providers/Extensions/OAuthServiceProvider.php @@ -0,0 +1,39 @@ +app->singleton(OAuthService::class, function ($app) { + $service = new OAuthService(); + + // Default OAuth providers included with Socialite + $service->register(new CommonSchema('facebook', icon: 'tabler-brand-facebook-f', hexColor: '#1877f2')); + $service->register(new CommonSchema('x', icon: 'tabler-brand-x-f', hexColor: '#1da1f2')); + $service->register(new CommonSchema('linkedin', icon: 'tabler-brand-linkedin-f', hexColor: '#0a66c2')); + $service->register(new CommonSchema('google', icon: 'tabler-brand-google-f', hexColor: '#4285f4')); + $service->register(new GithubSchema()); + $service->register(new GitlabSchema()); + $service->register(new CommonSchema('bitbucket', icon: 'tabler-brand-bitbucket-f', hexColor: '#205081')); + $service->register(new CommonSchema('slack', icon: 'tabler-brand-slack', hexColor: '#6ecadc')); + + // Additional OAuth providers from socialiteproviders.com + $service->register(new AuthentikSchema()); + $service->register(new DiscordSchema()); + $service->register(new SteamSchema()); + + return $service; + }); + } +} diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index 6561b6401..61e31cd1c 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -2,12 +2,14 @@ namespace App\Services\Servers; +use App\Extensions\Features\FeatureService; +use App\Models\Egg; use App\Models\Mount; use App\Models\Server; class ServerConfigurationStructureService { - public function __construct(private EnvironmentService $environment) {} + public function __construct(private EnvironmentService $environment, private FeatureService $featureService) {} /** * Return a configuration array for a specific server when passed a server model. @@ -58,7 +60,7 @@ class ServerConfigurationStructureService * default: array{ip: string, port: int}, * mappings: array>, * }, - * egg: array{id: string, file_denylist: string[]}, + * egg: array{id: string, file_denylist: string[], features: string[][]}, * labels?: string[], * mounts: array{source: string, target: string, read_only: bool}, * } @@ -101,9 +103,7 @@ class ServerConfigurationStructureService 'egg' => [ 'id' => $server->egg->uuid, 'file_denylist' => $server->egg->inherit_file_denylist, - 'features' => collect($server->egg->features())->mapWithKeys(fn ($feature) => [ - $feature->getId() => $feature->getListeners(), - ])->all(), + 'features' => $this->featureService->getMappings($server->egg->features), ], ]; diff --git a/bootstrap/providers.php b/bootstrap/providers.php index bd18d9423..62fd6a953 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -8,6 +8,10 @@ return [ App\Providers\Filament\AdminPanelProvider::class, App\Providers\Filament\AppPanelProvider::class, App\Providers\Filament\ServerPanelProvider::class, + App\Providers\Extensions\AvatarServiceProvider::class, + App\Providers\Extensions\CaptchaServiceProvider::class, + App\Providers\Extensions\FeatureServiceProvider::class, + App\Providers\Extensions\OAuthServiceProvider::class, App\Providers\RouteServiceProvider::class, SocialiteProviders\Manager\ServiceProvider::class, ]; diff --git a/phpstan.neon b/phpstan.neon index 3320b0842..7611b9b73 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -20,6 +20,6 @@ parameters: identifier: larastan.noEnvCallsOutsideOfConfig paths: - app/Console/Commands/Environment/*.php - - app/Extensions/Captcha/Providers/*.php - - app/Extensions/OAuth/Providers/*.php + - app/Extensions/Captcha/Schemas/*.php + - app/Extensions/OAuth/Schemas/*.php - app/Filament/Admin/Pages/Settings.php