diff --git a/.env b/.env index ca9cade..fdf78e0 100644 --- a/.env +++ b/.env @@ -51,3 +51,11 @@ OIDC_CLIENT_ID="Enter your OIDC client id" OIDC_CLIENT_SECRET="Enter your OIDC client secret" OIDC_BYPASS_FORM_LOGIN=false ###< drenso/symfony-oidc-bundle ### + + +SMTP_HOST= +SMTP_USER= +SMTP_PASS= +SMTP_PORT= +SMTP_FROM= +SMTP_FROM_NAME="" \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index b8885fa..491c335 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -6,6 +6,7 @@ parameters: # App app.url: '%env(APP_URL)%' + app.env: '%env(default:app.default.env:APP_ENV)%' app.version: '%env(default:app.default.version:APP_VERSION)%' # Debrid Services @@ -32,6 +33,7 @@ parameters: app.cache.redis.host.default: 'redis://redis' # Various configs + app.default.env: 'prod' app.default.version: '0.dev' app.default.timezone: 'America/Chicago' diff --git a/src/Base/Config/AppConfig.php b/src/Base/Config/AppConfig.php new file mode 100644 index 0000000..0e6d4c8 --- /dev/null +++ b/src/Base/Config/AppConfig.php @@ -0,0 +1,105 @@ +isVariableValid('APP_ENV', $this->appEnv)) { + $valid = false; + } + + if (false === $this->isVariableValid('APP_URL', $this->appUrl)) { + $valid = false; + } + + if (false === $this->isVariableValid('REAL_DEBRID_KEY', $this->realDebridApiKey)) { + $valid = false; + } + + if (false === $this->isVariableValid('TMDB_API', $this->tmdbApiKey)) { + $valid = false; + } + + if (false === $this->isVariableValid('MOVIES_PATH', $this->moviesPath)) { + $valid = false; + } + + if (false === $this->isVariableValid('TVSHOWS_PATH', $this->tvshowsPath)) { + $valid = false; + } + + return $valid; + } + + public function getMessages(): array + { + return $this->messages; + } + + private function isVariableValid($key, $value): bool + { + if ("" === $value || null === $value) { + $this->messages[] = "Your system is misconfigured. Please set the $key environment variable appropriately."; + return false; + } + return true; + } + + public function getAppEnv(): ?string + { + return $this->appEnv; + } + + public function getAppUrl(): ?string + { + return $this->appUrl; + } + + public function getRealDebridApiKey(): ?string + { + return $this->realDebridApiKey; + } + + public function getTmdbApiKey(): ?string + { + return $this->tmdbApiKey; + } + + public function getMoviesPath(): ?string + { + return $this->moviesPath; + } + + public function getTvshowsPath(): ?string + { + return $this->tvshowsPath; + } +} diff --git a/src/Base/Config/Auth/OidcConfig.php b/src/Base/Config/Auth/OidcConfig.php new file mode 100644 index 0000000..ef7b242 --- /dev/null +++ b/src/Base/Config/Auth/OidcConfig.php @@ -0,0 +1,88 @@ +authMethod); + } + + public function isValid(): bool + { + $valid = true; + + if (true === $this->isEnabled()) { + if (false === $this->isVariableValid("OIDC_CLIENT_ID", $this->clientId)) { + $valid = false; + } + + if (false === $this->isVariableValid("OIDC_CLIENT_SECRET", $this->clientSecret)) { + $valid = false; + } + + if (false === $this->isVariableValid("OIDC_WELL_KNOWN_URL", $this->wellKnownUrl)) { + $valid = false; + } + } + + return $valid; + } + + public function getWellKnownUrl(): ?string + { + return $this->wellKnownUrl; + } + + public function getClientId(): ?string + { + return $this->clientId; + } + + public function getClientSecret(): ?string + { + return $this->clientSecret; + } + + public function getBypassFormLogin(): ?bool + { + return $this->bypassFormLogin; + } + + public function getMessages(): array + { + return $this->messages; + } + + private function isVariableValid(string $key, mixed $value): bool + { + if ("" === $value || null === $value) { + $this->messages[] = "Your OIDC is misconfigured. Please set the $key environment variable to the required value."; + return true; + } + return false; + } +} diff --git a/src/Base/Config/AuthConfig.php b/src/Base/Config/AuthConfig.php new file mode 100644 index 0000000..52d6840 --- /dev/null +++ b/src/Base/Config/AuthConfig.php @@ -0,0 +1,62 @@ +getAuthMethod() === strtolower($method); + } + + public function getAuthMethod(): string + { + return strtolower($this->authMethod); + } + + public function getOidcConfig(): OidcConfig + { + return $this->oidcConfig; + } + + public function isValid(): bool + { + $valid = true; + if (null === $this->getAuthMethod() || "" === $this->getAuthMethod()) { + $this->messages[] = "Your auth method is missing. Please set the AUTH_METHOD environment variable to your desired value. Valid options: [form_login, oidc]."; + return false; + } + + if (!in_array($this->getAuthMethod(), self::AUTH_METHODS, true)) { + $this->messages[] = "Your auth method is incorrect. Please set the AUTH_METHOD environment variable to your desired value. Valid options: [form_login, oidc]."; + return false; + } + + if ("oidc" === $this->getAuthMethod()) { + if (false === $this->oidcConfig->isValid()) { + $this->messages += $this->oidcConfig->getMessages(); + $valid = false; + } + } + + return $valid; + } + + public function getMessages(): array + { + return $this->messages; + } +} diff --git a/src/Base/Config/ConfigInterface.php b/src/Base/Config/ConfigInterface.php new file mode 100644 index 0000000..a615ed3 --- /dev/null +++ b/src/Base/Config/ConfigInterface.php @@ -0,0 +1,12 @@ + $value) { + if ("" !== $value && $value !== null) { + $this->isEnabled = true; + } + } + } + + public function isEnabled(): bool + { + return $this->isEnabled; + } + + public function getMessages(): array + { + return $this->messages; + } + + public function isValid(): bool + { + if (false === $this->isEnabled) { + return true; + } + + $valid = true; + + $params = [ + 'SMTP_HOST' => $this->smtpHost, + 'SMTP_USER' => $this->smtpUser, + 'SMTP_PASS' => $this->smtpPass, + 'SMTP_PORT' => $this->smtpPort, + 'SMTP_FROM' => $this->smtpFrom, + 'SMTP_FROM_NAME' => $this->smtpFromName, + ]; + + foreach ($params as $key => $value) { + if (false === $this->isVariableValid($key, $value)) { + $valid = false; + } + } + + return $valid; + } + + private function isVariableValid($key, $value): bool + { + if ("" === $value || null === $value) { + $this->messages[] = "Your SMTP is misconfigured. Please set the $key environment variable appropriately."; + return false; + } + return true; + } + + public function getSmtpHost(): ?string + { + return $this->smtpHost; + } + + public function getSmtpUser(): ?string + { + return $this->smtpUser; + } + + public function getSmtpPass(): ?string + { + return $this->smtpPass; + } + + public function getSmtpPort(): ?string + { + return $this->smtpPort; + } + + public function getSmtpFrom(): ?string + { + return $this->smtpFrom; + } + + public function getSmtpFromName(): ?string + { + return $this->smtpFromName; + } +} diff --git a/src/Base/ConfigResolver.php b/src/Base/ConfigResolver.php index 520dc49..612955f 100644 --- a/src/Base/ConfigResolver.php +++ b/src/Base/ConfigResolver.php @@ -2,58 +2,60 @@ namespace App\Base; -use Symfony\Component\DependencyInjection\Attribute\Autowire; +use App\Base\Config\AppConfig; +use App\Base\Config\AuthConfig; +use App\Base\Config\SmtpConfig; +use Psr\Log\LoggerInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; final class ConfigResolver { private array $messages = []; public function __construct( - #[Autowire(param: 'app.url')] - private readonly ?string $appUrl = null, - - #[Autowire(param: 'app.debrid.real_debrid.key')] - private readonly ?string $realDebridApiKey = null, - - #[Autowire(param: 'app.meta_provider.tmdb.key')] - private readonly ?string $tmdbApiKey = null, - - #[Autowire(param: 'media.movies_path')] - private readonly ?string $moviesPath = null, - - #[Autowire(param: 'media.tvshows.path')] - private readonly ?string $tvshowsPath = null, - - #[Autowire(param: 'auth.method')] - private readonly ?string $authMethod = null, - - #[Autowire(param: 'auth.oidc.well_known_url')] - private readonly ?string $authOidcWellKnownUrl = null, - - #[Autowire(param: 'auth.oidc.client_id')] - private readonly ?string $authOidcClientId = null, - - #[Autowire(param: 'auth.oidc.client_secret')] - private readonly ?string $authOidcClientSecret = null, - - #[Autowire(param: 'auth.oidc.bypass_form_login')] - private ?bool $authOidcBypassFormLogin = null, + private readonly LoggerInterface $logger, + private readonly TagAwareCacheInterface $cache, + private readonly AuthConfig $authConfig, + private readonly SmtpConfig $smtpConfig, + private readonly AppConfig $appConfig, ) {} public function validate(): bool + { + if ("prod" === strtolower($this->appConfig->getAppEnv())) { + return $this->cache->get('app.valid_config', function () { + return $this->doValidate(); + }); + } else { + return $this->doValidate(); + } + } + + private function doValidate(): bool { $valid = true; - if (null === $this->realDebridApiKey || "" === $this->realDebridApiKey) { - $this->messages[] = "Your Real Debrid API key is missing. Please set it to the 'REAL_DEBRID_KEY' environment variable."; + if (false === $this->appConfig->isValid()) { + $this->messages += $this->appConfig->getMessages(); $valid = false; } - if (null === $this->tmdbApiKey || "" === $this->tmdbApiKey) { - $this->messages[] = "Your TMDB API key is missing. Please set it to the 'TMDB_API' environment variable."; + if (false === $this->authConfig->isValid()) { + $this->messages += $this->authConfig->getMessages(); $valid = false; } + if (false === $this->smtpConfig->isValid()) { + $this->messages += $this->smtpConfig->getMessages(); + $valid = false; + } + + if (false === $valid) { + foreach ($this->messages as $message) { + $this->logger->error('> [ConfigResolver] ' . $message); + } + } + return $valid; } @@ -62,34 +64,18 @@ final class ConfigResolver return $this->messages; } + public function getAuthConfig(): AuthConfig + { + return $this->authConfig; + } + public function authIs(string $method): bool { - if (strtolower($method) === strtolower($this->getAuthMethod())) { - return true; - } - return false; + return $this->authConfig->isMethod($method); } public function getAuthMethod(): string { - return strtolower($this->authMethod); - } - - public function bypassFormLogin(): bool - { - return $this->authOidcBypassFormLogin; - } - - public function getAuthConfig(): array - { - return [ - 'method' => $this->authMethod, - 'oidc' => [ - 'well_known_url' => $this->authOidcWellKnownUrl, - 'client_id' => $this->authOidcClientId, - 'client_secret' => $this->authOidcClientSecret, - 'bypass_form_login' => $this->authOidcBypassFormLogin, - ] - ]; + return $this->authConfig->getAuthMethod(); } } diff --git a/src/Base/Framework/EventListener/KernelRequestValidateConfig.php b/src/Base/Framework/EventListener/KernelRequestValidateConfig.php new file mode 100644 index 0000000..2e1e6c7 --- /dev/null +++ b/src/Base/Framework/EventListener/KernelRequestValidateConfig.php @@ -0,0 +1,24 @@ +configResolver->validate()) { + $this->broadcaster->systemAlert('Ruh-roh', 'It looks like your system is misconfigured. Please search the application logs for strings starting with "[error] > [ConfigResolver]" to find more information.'); + } + } +} diff --git a/src/Base/Service/Broadcaster.php b/src/Base/Service/Broadcaster.php index b3d6c6c..15cf93c 100644 --- a/src/Base/Service/Broadcaster.php +++ b/src/Base/Service/Broadcaster.php @@ -15,7 +15,8 @@ readonly class Broadcaster private Environment $renderer, private HubInterface $hub, private RequestStack $requestStack, - ) {} + ) { + } public function alert(string $title, string $message, string $type = "success"): void { @@ -31,4 +32,18 @@ readonly class Broadcaster ); $this->hub->publish($update); } + + public function systemAlert(string $title, string $message, string $type = "warning"): void + { + $update = new Update( + 'system_alerts', + $this->renderer->render('broadcast/Alert.stream.html.twig', [ + 'alert_id' => uniqid(), + 'title' => $title, + 'message' => $message, + 'type' => $type, + ]) + ); + $this->hub->publish($update); + } } diff --git a/src/User/Framework/Controller/Web/LoginController.php b/src/User/Framework/Controller/Web/LoginController.php index b96b731..4981749 100644 --- a/src/User/Framework/Controller/Web/LoginController.php +++ b/src/User/Framework/Controller/Web/LoginController.php @@ -22,7 +22,7 @@ class LoginController extends AbstractController return $this->redirectToRoute('app_getting_started'); } - if ($config->authIs('oidc') && $config->bypassFormLogin()) { + if ($config->authIs('oidc') && true === $config->getAuthConfig()->getOidcConfig()->getBypassFormLogin()) { return $this->redirectToRoute('app_login_oidc'); } diff --git a/src/User/Framework/Controller/Web/LoginOidcController.php b/src/User/Framework/Controller/Web/LoginOidcController.php index 9e5d089..1955b60 100644 --- a/src/User/Framework/Controller/Web/LoginOidcController.php +++ b/src/User/Framework/Controller/Web/LoginOidcController.php @@ -3,6 +3,7 @@ namespace App\User\Framework\Controller\Web; use App\Base\ConfigResolver; +use App\Base\Service\Broadcaster; use Drenso\OidcBundle\OidcClientInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; @@ -15,13 +16,15 @@ class LoginOidcController extends AbstractController public function __construct( private ConfigResolver $configResolver, + private Broadcaster $broadcaster, ) {} #[Route('/login/oidc', name: 'app_login_oidc')] public function oidcStart(OidcClientInterface $oidcClient): RedirectResponse { if (false === $this->configResolver->authIs('oidc')) { - throw new \Exception('You must configure the OIDC environment variables before logging in at this route.'); + $this->broadcaster->systemAlert('Your authentication must be set to "oidc" in order to login with OIDC.', 'warning'); + return $this->redirectToRoute('app_login'); } // Redirect to authorization @ OIDC provider diff --git a/src/User/Framework/Security/OidcUserProvider.php b/src/User/Framework/Security/OidcUserProvider.php index f85ea7d..e8103a2 100644 --- a/src/User/Framework/Security/OidcUserProvider.php +++ b/src/User/Framework/Security/OidcUserProvider.php @@ -4,13 +4,9 @@ namespace App\User\Framework\Security; use App\User\Framework\Entity\User; use App\User\Framework\Repository\UserRepository; -use Drenso\OidcBundle\Exception\OidcException; use Drenso\OidcBundle\Model\OidcTokens; use Drenso\OidcBundle\Model\OidcUserData; use Drenso\OidcBundle\Security\UserProvider\OidcUserProviderInterface; -use Symfony\Component\PasswordHasher\PasswordHasherInterface; -use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\OidcUser; use Symfony\Component\Security\Core\User\UserInterface; @@ -47,7 +43,7 @@ class OidcUserProvider implements OidcUserProviderInterface public function supportsClass(string $class): bool { - return User::class === $class || OidcUser::class === $class; + return User::class === $class || OidcUser::class === $class || is_subclass_of($class, User::class); } public function loadUserByIdentifier(string $identifier): UserInterface diff --git a/templates/bare.html.twig b/templates/bare.html.twig index b03b45b..acfaff7 100644 --- a/templates/bare.html.twig +++ b/templates/bare.html.twig @@ -23,5 +23,17 @@ v{{ version }} +