diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae6cf1d..0cd3c78e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add POST '/token' endpoint. - Add expiration date to tokens. +### Changed +- Reword application configuration, via internal Symfony bundle. +- Switch datetime format from temporary string type to datetime. + ## 0.0.19 - 2023-08-10 ### Added - Add `/instance-configuration` endpoint for retrieving instance specific configurations. diff --git a/composer.json b/composer.json index 186a4bb1..44e176bf 100644 --- a/composer.json +++ b/composer.json @@ -60,6 +60,7 @@ }, "autoload": { "psr-4": { + "EmberNexusBundle\\": "lib/EmberNexusBundle/src/", "App\\": "src/" } }, diff --git a/config/bundles.php b/config/bundles.php index 53215888..3f27240d 100755 --- a/config/bundles.php +++ b/config/bundles.php @@ -7,4 +7,5 @@ Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], League\FlysystemBundle\FlysystemBundle::class => ['all' => true], + EmberNexusBundle\EmberNexusBundle::class => ['all' => true], ]; diff --git a/config/default-parameters.yaml b/config/default-parameters.yaml index bfba487c..2b6c510a 100644 --- a/config/default-parameters.yaml +++ b/config/default-parameters.yaml @@ -21,7 +21,7 @@ parameters: register: # If true, the /register endpoint is active and anonymous users can create accounts. - enabled: false + enabled: true # The property name of the identifier. Identifier must be unique across the API, usually the email. uniqueIdentifier: email diff --git a/config/packages/ember_nexus.yaml b/config/packages/ember_nexus.yaml new file mode 100644 index 00000000..271f1626 --- /dev/null +++ b/config/packages/ember_nexus.yaml @@ -0,0 +1,5 @@ +ember_nexus: + register: + enabled: true + instanceConfiguration: + showVersion: false diff --git a/lib/EmberNexusBundle/src/DependencyInjection/Configuration.php b/lib/EmberNexusBundle/src/DependencyInjection/Configuration.php new file mode 100644 index 00000000..e0a3e415 --- /dev/null +++ b/lib/EmberNexusBundle/src/DependencyInjection/Configuration.php @@ -0,0 +1,106 @@ +getRootNode(); + $rootNode + ->children() + + ->arrayNode('pageSize') + ->info('Affects how many elements can be returned in collection responses.') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('min') + ->info('Minimum number of elements which are always returned, if they exist.') + ->min(1) + ->defaultValue(5) + ->end() + ->integerNode('default') + ->info('Default number of elements which are returned if they exist.') + ->min(1) + ->defaultValue(25) + ->end() + ->integerNode('max') + ->info('Maximum number of elements which are returned in a single response. Should not be way more than 100, as performance problems may arise.') + ->min(1) + ->defaultValue(100) + ->end() + ->end() + ->end() + + ->arrayNode('register') + ->info('Handles the /register endpoint.') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled') + ->info('If true, the /register endpoint is active and anonymous users can create accounts.') + ->defaultTrue() + ->end() + ->scalarNode('uniqueIdentifier') + ->info('The property name of the identifier. Identifier must be unique across the API, usually the email.') + ->defaultValue('email') + ->end() + ->integerNode('uniqueIdentifierRegex') + ->info('Either false or a regex for checking the identifier content.') + ->defaultFalse() + ->end() + ->end() + ->end() + + ->arrayNode('instanceConfiguration') + ->info('Configures the /instance-configuration endpoint') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled') + ->info('If true, enables the endpoint. If false, 403 error messages are returned.') + ->defaultTrue() + ->end() + ->scalarNode('showVersion') + ->info('If false, the version number is omitted.') + ->defaultTrue() + ->end() + ->end() + ->end() + + ->arrayNode('token') + ->info('Configures the /instance-configuration endpoint') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('minLifetimeInSeconds') + ->info('Minimum lifetime of created tokens.') + ->defaultValue(self::HALF_AN_HOUR_IN_SECONDS) + ->end() + ->integerNode('defaultLifetimeInSeconds') + ->info('Default lifetime of created tokens.') + ->defaultValue(self::THREE_HOURS_IN_SECONDS) + ->end() + ->scalarNode('maxLifetimeInSeconds') + ->info('Maximum lifetime of created tokens. Can be set to false to disable maximum limit.') + ->defaultValue(self::THIRTEEN_MONTHS_IN_SECONDS) + ->end() + ->scalarNode('deleteExpiredTokensAutomaticallyInSeconds') + ->info('Expired tokens will be deleted after defined time. Can be set to false to disable auto delete feature.') + ->defaultValue(self::TWO_WEEKS_IN_SECONDS) + ->end() + ->end() + ->end() + + ->end() + ; + + return $treeBuilder; + } +} diff --git a/lib/EmberNexusBundle/src/DependencyInjection/EmberNexusExtension.php b/lib/EmberNexusBundle/src/DependencyInjection/EmberNexusExtension.php new file mode 100644 index 00000000..e8e276c2 --- /dev/null +++ b/lib/EmberNexusBundle/src/DependencyInjection/EmberNexusExtension.php @@ -0,0 +1,29 @@ +getConfiguration($rawConfigurations, $container); + $processedConfiguration = $this->processConfiguration($configuration, $rawConfigurations); + + $container->setDefinition( + EmberNexusConfiguration::class, + (new Definition()) + ->setFactory([null, 'createFromConfiguration']) + ->setArguments([$processedConfiguration]) + ); + } +} diff --git a/lib/EmberNexusBundle/src/EmberNexusBundle.php b/lib/EmberNexusBundle/src/EmberNexusBundle.php new file mode 100644 index 00000000..1d905fdf --- /dev/null +++ b/lib/EmberNexusBundle/src/EmberNexusBundle.php @@ -0,0 +1,9 @@ +setPageSizeMin((int) $configuration['pageSize']['min']); + if (!array_key_exists('default', $configuration['pageSize'])) { + throw new \Exception("Configuration must contain key 'pageSize.default'."); + } + $emberNexusConfiguration->setPageSizeDefault((int) $configuration['pageSize']['default']); + if (!array_key_exists('max', $configuration['pageSize'])) { + throw new \Exception("Configuration must contain key 'pageSize.max'."); + } + $emberNexusConfiguration->setPageSizeMax((int) $configuration['pageSize']['max']); + + if ($emberNexusConfiguration->getPageSizeMax() < $emberNexusConfiguration->getPageSizeMin()) { + throw new \Exception('pagesize max must be smaller or equal to pagesize min.'); + } + if ($emberNexusConfiguration->getPageSizeDefault() < $emberNexusConfiguration->getPageSizeMin()) { + throw new \Exception('default page size must be at least as big as min pagesize'); + } + if ($emberNexusConfiguration->getPageSizeMax() < $emberNexusConfiguration->getPageSizeDefault()) { + throw new \Exception('default page size must be equal or less than max page size.'); + } + + if (!array_key_exists('register', $configuration)) { + throw new \Exception("Configuration must contain key 'register'."); + } + if (!array_key_exists('enabled', $configuration['register'])) { + throw new \Exception("Configuration must contain key 'register.enabled'."); + } + $emberNexusConfiguration->setRegisterEnabled((bool) $configuration['register']['enabled']); + if (!array_key_exists('uniqueIdentifier', $configuration['register'])) { + throw new \Exception("Configuration must contain key 'register.uniqueIdentifier'."); + } + $emberNexusConfiguration->setRegisterUniqueIdentifier((string) $configuration['register']['uniqueIdentifier']); + if (!array_key_exists('uniqueIdentifierRegex', $configuration['register'])) { + throw new \Exception("Configuration must contain key 'register.uniqueIdentifierRegex'."); + } + $emberNexusConfiguration->setRegisterUniqueIdentifierRegex((string) $configuration['register']['uniqueIdentifierRegex']); + + if (!array_key_exists('instanceConfiguration', $configuration)) { + throw new \Exception("Configuration must contain key 'instanceConfiguration'."); + } + if (!array_key_exists('enabled', $configuration['instanceConfiguration'])) { + throw new \Exception("Configuration must contain key 'instanceConfiguration.enabled'."); + } + $emberNexusConfiguration->setInstanceConfigurationEnabled((bool) $configuration['instanceConfiguration']['enabled']); + if (!array_key_exists('showVersion', $configuration['instanceConfiguration'])) { + throw new \Exception("Configuration must contain key 'instanceConfiguration.showVersion'."); + } + $emberNexusConfiguration->setInstanceConfigurationShowVersion((bool) $configuration['instanceConfiguration']['showVersion']); + + if (!array_key_exists('token', $configuration)) { + throw new \Exception("Configuration must contain key 'token'."); + } + if (!array_key_exists('minLifetimeInSeconds', $configuration['token'])) { + throw new \Exception("Configuration must contain key 'token.minLifetimeInSeconds'."); + } + $emberNexusConfiguration->setTokenMinLifetimeInSeconds((int) $configuration['token']['minLifetimeInSeconds']); + if (!array_key_exists('defaultLifetimeInSeconds', $configuration['token'])) { + throw new \Exception("Configuration must contain key 'token.defaultLifetimeInSeconds'."); + } + $emberNexusConfiguration->setTokenDefaultLifetimeInSeconds((int) $configuration['token']['defaultLifetimeInSeconds']); + if (!array_key_exists('maxLifetimeInSeconds', $configuration['token'])) { + throw new \Exception("Configuration must contain key 'token.maxLifetimeInSeconds'."); + } + $value = $configuration['token']['maxLifetimeInSeconds']; + if (false !== $value) { + $value = (int) $value; + } + $emberNexusConfiguration->setTokenMaxLifetimeInSeconds($value); + if (!array_key_exists('deleteExpiredTokensAutomaticallyInSeconds', $configuration['token'])) { + throw new \Exception("Configuration must contain key 'token.deleteExpiredTokensAutomaticallyInSeconds'."); + } + $value = $configuration['token']['deleteExpiredTokensAutomaticallyInSeconds']; + if (false !== $value) { + $value = (int) $value; + } + $emberNexusConfiguration->setTokenDeleteExpiredTokensAutomaticallyInSeconds($value); + + if (false !== $emberNexusConfiguration->getTokenMaxLifetimeInSeconds()) { + if ($emberNexusConfiguration->getTokenMaxLifetimeInSeconds() < $emberNexusConfiguration->getTokenMinLifetimeInSeconds()) { + throw new \Exception('token max lifetime must be longer than min lifetime.'); + } + if ($emberNexusConfiguration->getTokenDefaultLifetimeInSeconds() > $emberNexusConfiguration->getTokenMaxLifetimeInSeconds()) { + throw new \Exception('Token default lifetime must by shorter or equal to max lifetime.'); + } + } + if ($emberNexusConfiguration->getTokenDefaultLifetimeInSeconds() < $emberNexusConfiguration->getTokenMinLifetimeInSeconds()) { + throw new \Exception('token default lifetime must be equal or longer to min lifetime.'); + } + + return $emberNexusConfiguration; + } + + public function getPageSizeMin(): int + { + return $this->pageSizeMin; + } + + public function setPageSizeMin(int $pageSizeMin): EmberNexusConfiguration + { + $this->pageSizeMin = $pageSizeMin; + + return $this; + } + + public function getPageSizeDefault(): int + { + return $this->pageSizeDefault; + } + + public function setPageSizeDefault(int $pageSizeDefault): EmberNexusConfiguration + { + $this->pageSizeDefault = $pageSizeDefault; + + return $this; + } + + public function getPageSizeMax(): int + { + return $this->pageSizeMax; + } + + public function setPageSizeMax(int $pageSizeMax): EmberNexusConfiguration + { + $this->pageSizeMax = $pageSizeMax; + + return $this; + } + + public function isRegisterEnabled(): bool + { + return $this->registerEnabled; + } + + public function setRegisterEnabled(bool $registerEnabled): EmberNexusConfiguration + { + $this->registerEnabled = $registerEnabled; + + return $this; + } + + public function getRegisterUniqueIdentifier(): string + { + return $this->registerUniqueIdentifier; + } + + public function setRegisterUniqueIdentifier(string $registerUniqueIdentifier): EmberNexusConfiguration + { + if (0 === strlen($registerUniqueIdentifier)) { + throw new \Exception('Unique identifier can not be an empty string.'); + } + $this->registerUniqueIdentifier = $registerUniqueIdentifier; + + return $this; + } + + public function getRegisterUniqueIdentifierRegex(): ?string + { + return $this->registerUniqueIdentifierRegex; + } + + public function setRegisterUniqueIdentifierRegex(?string $registerUniqueIdentifierRegex): EmberNexusConfiguration + { + $this->registerUniqueIdentifierRegex = $registerUniqueIdentifierRegex; + + return $this; + } + + public function isInstanceConfigurationEnabled(): bool + { + return $this->instanceConfigurationEnabled; + } + + public function setInstanceConfigurationEnabled(bool $instanceConfigurationEnabled): EmberNexusConfiguration + { + $this->instanceConfigurationEnabled = $instanceConfigurationEnabled; + + return $this; + } + + public function isInstanceConfigurationShowVersion(): bool + { + return $this->instanceConfigurationShowVersion; + } + + public function setInstanceConfigurationShowVersion(bool $instanceConfigurationShowVersion): EmberNexusConfiguration + { + $this->instanceConfigurationShowVersion = $instanceConfigurationShowVersion; + + return $this; + } + + public function getTokenMinLifetimeInSeconds(): int + { + return $this->tokenMinLifetimeInSeconds; + } + + public function setTokenMinLifetimeInSeconds(int $tokenMinLifetimeInSeconds): EmberNexusConfiguration + { + $this->tokenMinLifetimeInSeconds = $tokenMinLifetimeInSeconds; + + return $this; + } + + public function getTokenDefaultLifetimeInSeconds(): int + { + return $this->tokenDefaultLifetimeInSeconds; + } + + public function setTokenDefaultLifetimeInSeconds(int $tokenDefaultLifetimeInSeconds): EmberNexusConfiguration + { + $this->tokenDefaultLifetimeInSeconds = $tokenDefaultLifetimeInSeconds; + + return $this; + } + + public function getTokenMaxLifetimeInSeconds(): bool|int + { + return $this->tokenMaxLifetimeInSeconds; + } + + public function setTokenMaxLifetimeInSeconds(bool|int $tokenMaxLifetimeInSeconds): EmberNexusConfiguration + { + $this->tokenMaxLifetimeInSeconds = $tokenMaxLifetimeInSeconds; + + return $this; + } + + public function getTokenDeleteExpiredTokensAutomaticallyInSeconds(): bool|int + { + return $this->tokenDeleteExpiredTokensAutomaticallyInSeconds; + } + + public function setTokenDeleteExpiredTokensAutomaticallyInSeconds(bool|int $tokenDeleteExpiredTokensAutomaticallyInSeconds): EmberNexusConfiguration + { + $this->tokenDeleteExpiredTokensAutomaticallyInSeconds = $tokenDeleteExpiredTokensAutomaticallyInSeconds; + + return $this; + } +} diff --git a/src/EventListener/CreatedElementPreWriteEventListener.php b/src/EventListener/CreatedElementPreWriteEventListener.php index ce34d91d..f1bf0247 100644 --- a/src/EventListener/CreatedElementPreWriteEventListener.php +++ b/src/EventListener/CreatedElementPreWriteEventListener.php @@ -23,7 +23,6 @@ private function handleEvent(ElementPreCreateEvent|ElementPreMergeEvent $event): if ($element->hasProperty('created')) { return; } - $now = (new \DateTime())->format('Y-m-d H:i:s'); - $element->addProperty('created', $now); + $element->addProperty('created', new \DateTime()); } } diff --git a/src/EventListener/GenericPropertyElementFragmentizeEventListener.php b/src/EventListener/GenericPropertyElementFragmentizeEventListener.php index 60f4cd90..e2fe054a 100755 --- a/src/EventListener/GenericPropertyElementFragmentizeEventListener.php +++ b/src/EventListener/GenericPropertyElementFragmentizeEventListener.php @@ -37,6 +37,11 @@ private function handleEvent(NodeElementFragmentizeEvent|RelationElementFragment $mongoFragment->addProperty($name, $value); continue; } + if ($value instanceof \DateTimeInterface) { + $cypherFragment->addProperty($name, $value); + $elasticFragment->addProperty($name, $value->format('Uu')); + continue; + } if (is_object($value)) { $mongoFragment->addProperty($name, $value); continue; diff --git a/src/EventListener/UpdatedElementPreWriteEventListener.php b/src/EventListener/UpdatedElementPreWriteEventListener.php index eba46447..4ec72bd3 100644 --- a/src/EventListener/UpdatedElementPreWriteEventListener.php +++ b/src/EventListener/UpdatedElementPreWriteEventListener.php @@ -23,7 +23,6 @@ private function handleEvent(ElementPreCreateEvent|ElementPreMergeEvent $event): if ($element->hasProperty('updated')) { return; } - $now = (new \DateTime())->format('Y-m-d H:i:s'); - $element->addProperty('updated', $now); + $element->addProperty('updated', new \DateTime()); } } diff --git a/src/Security/TokenGenerator.php b/src/Security/TokenGenerator.php index 7b381003..65aee492 100755 --- a/src/Security/TokenGenerator.php +++ b/src/Security/TokenGenerator.php @@ -5,11 +5,11 @@ use App\Service\ElementManager; use App\Type\NodeElement; use App\Type\RelationElement; +use EmberNexusBundle\Service\EmberNexusConfiguration; use Laudis\Neo4j\Databags\Statement; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Safe\DateTime; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Syndesi\CypherEntityManager\Type\EntityManager as CypherEntityManager; use Tuupola\Base58; @@ -20,7 +20,7 @@ class TokenGenerator public function __construct( private ElementManager $elementManager, private CypherEntityManager $cypherEntityManager, - private ParameterBagInterface $bag + private EmberNexusConfiguration $emberNexusConfiguration ) { $this->encoder = new Base58(); } @@ -44,29 +44,17 @@ public function createNewToken(UuidInterface $userUuid, string $name = null, int } } - $tokenConfig = $this->bag->get('token'); - if (null === $tokenConfig) { - throw new \Exception("Unable to get config; key 'token' must exist."); - } - if (!is_array($tokenConfig)) { - throw new \Exception("Configuration key 'token' must be an array."); - } - if (null === $lifetimeInSeconds) { - $lifetimeInSeconds = $tokenConfig['defaultLifetimeInSeconds']; - } - - if ($lifetimeInSeconds > $tokenConfig['maxLifetimeInSeconds']) { - $lifetimeInSeconds = $tokenConfig['maxLifetimeInSeconds']; - } - if ($lifetimeInSeconds < $tokenConfig['minLifetimeInSeconds']) { - $lifetimeInSeconds = $tokenConfig['minLifetimeInSeconds']; + $lifetimeInSeconds = $this->emberNexusConfiguration->getTokenDefaultLifetimeInSeconds(); + } else { + if ($lifetimeInSeconds > $this->emberNexusConfiguration->getTokenMaxLifetimeInSeconds()) { + $lifetimeInSeconds = $this->emberNexusConfiguration->getTokenMaxLifetimeInSeconds(); + } + if ($lifetimeInSeconds < $this->emberNexusConfiguration->getTokenMinLifetimeInSeconds()) { + $lifetimeInSeconds = $this->emberNexusConfiguration->getTokenMinLifetimeInSeconds(); + } } - $expirationDate = (new DateTime()) - ->add(new \DateInterval(sprintf('PT%sS', $lifetimeInSeconds))) - ->format('Y-m-d H:i:s'); - $name ??= (new DateTime())->format('Y-m-d H:i:s'); $tokenUuid = Uuid::uuid4(); @@ -75,7 +63,7 @@ public function createNewToken(UuidInterface $userUuid, string $name = null, int ->setIdentifier($tokenUuid) ->addProperties([ 'hash' => $hash, - 'expirationDate' => $expirationDate, + 'expirationDate' => (new DateTime())->add(new \DateInterval(sprintf('PT%sS', $lifetimeInSeconds))), 'name' => $name, ]); $this->elementManager->create($tokenNode);