partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 88eda40453e03929b3d0748fdc8d60398e869360
parent a48e9179e0f17b50c8300cb3726744fe1fde8df7
Author: Felicitus <felicitus@felicitus.org>
Date:   Fri,  2 Oct 2015 14:22:31 +0200

Reworked authentication implementation, migrated user editor

Diffstat:
Mapp/AppKernel.php | 3++-
Mapp/config/config.yml | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mapp/config/partkeepr.yml | 2+-
Mapp/config/security.yml | 6+++++-
Mcomposer.json | 1+
Asrc/PartKeepr/AuthBundle/Action/DeletePreferenceAction.php | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/AuthBundle/Action/PostUserAction.php | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/AuthBundle/Action/PutUserAction.php | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/AuthBundle/Entity/FOSUser.php | 25+++++++++++++++++++++++++
Msrc/PartKeepr/AuthBundle/Entity/User.php | 114+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Asrc/PartKeepr/AuthBundle/Entity/UserProvider.php | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/AuthBundle/Resources/config/actions.xml | 20++++++++++++++++++--
Msrc/PartKeepr/AuthBundle/Resources/config/services.xml | 2++
Msrc/PartKeepr/AuthBundle/Security/Authentication/AuthenticationProviderManager.php | 9++++++---
Msrc/PartKeepr/AuthBundle/Services/UserService.php | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/PartKeepr/AuthBundle/Tests/Services/UserPreferenceServiceTest.php | 21+++++++++++++++------
Msrc/PartKeepr/CoreBundle/DependencyInjection/Configuration.php | 10+++++-----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorComponent.js | 5-----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Statusbar.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserEditor.js | 70+++++++++++++++++++++++++++++-----------------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserEditorComponent.js | 49+++++++++++++++++++++++++++----------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserGrid.js | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/UserPreferenceGrid.js | 33++++++++++++---------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js | 39+++++++++++++++++++++++++++------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/UserProvidersStore.js | 20++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/views/index.html.twig | 1+
26 files changed, 822 insertions(+), 182 deletions(-)

diff --git a/app/AppKernel.php b/app/AppKernel.php @@ -70,7 +70,8 @@ class AppKernel extends Kernel new Brainbits\FugueIconsBundle\BrainbitsFugueIconsBundle(), new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(), new JMS\TranslationBundle\JMSTranslationBundle(), - new \PartKeepr\RemoteFileLoader\PartKeeprRemoteFileLoaderBundle() + new \PartKeepr\RemoteFileLoader\PartKeeprRemoteFileLoaderBundle(), + new \FR3D\LdapBundle\FR3DLdapBundle() ); // Developer bundles diff --git a/app/config/config.yml b/app/config/config.yml @@ -122,6 +122,29 @@ dunglas_api: items_per_page: client_can_change: true +fr3d_ldap: + driver: + host: "%fr3d_ldap.driver.host%" + port: "%fr3d_ldap.driver.port%" + username: "%fr3d_ldap.driver.username%" + password: "%fr3d_ldap.driver.password%" + bindRequiresDn: "%fr3d_ldap.driver.bindRequiresDn%" + baseDn: "%fr3d_ldap.driver.baseDn%" + accountFilterFormat: "%fr3d_ldap.driver.accountFilterFormat%" + optReferrals: "%fr3d_ldap.driver.optReferrals%" + useSsl: "%fr3d_ldap.driver.useSsl%" + useStartTls: "%fr3d_ldap.driver.useStartTls%" + accountCanonicalForm: "%fr3d_ldap.driver.accountCanonicalForm%" + accountDomainName: "%fr3d_ldap.driver.accountDomainName%" + accountDomainNameShort: "%fr3d_ldap.driver.accountDomainNameShort%" + user: + baseDn: "%fr3d_ldap.user.baseDn%" + filter: "%fr3d_ldap.user.filter%" + attributes: # Specify ldap attributes mapping [ldap attribute, user object method] + - { ldap_attr: samaccountname, user_method: setUsername } + - { ldap_attr: email, user_method: setEmail } + + services: serializer.normalizer.custom: class: Symfony\Component\Serializer\Normalizer\ObjectNormalizer @@ -943,7 +966,6 @@ services: - method: "initItemOperations" arguments: [ [ "@resource.tempfile.item_operation.get", "@resource.tempfile.item_operation.custom_get", "@resource.tempfile.item_operation.custom_get_mimetype", "@resource.tempfile.collection_operation.custom_post_webcam" ] ] - resource.tip_of_the_day.item_operation.get: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false @@ -1050,6 +1072,17 @@ services: - "partkeepr.user_preference.set_preference" # Controller - "PartKeeprUserPreferenceSet" + resource.user.item_operation.delete_preference: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createItemOperation" ] + arguments: + - "@resource.user" # Resource + - [ "DELETE" ] # Methods + - "/user_preferences" # Path + - "partkeepr.user_preference.delete_preference" # Controller + - "PartKeeprUserPreferenceDelete" + resource.user.item_operation.login: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false @@ -1061,17 +1094,44 @@ services: - "partkeepr.auth.login" # Controller - "PartKeeprAuthLogin" + resource.user.collection_operation.get_providers: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createItemOperation" ] + arguments: + - "@resource.user" # Resource + - [ "GET" ] # Methods + - "/users/get_user_providers" # Path + - "partkeepr.auth.get_providers" # Controller + - "PartKeeprAuthGetProviders" + resource.user.item_operation.get: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false factory: [ "@api.operation_factory", "createItemOperation" ] arguments: [ "@resource.user", "GET" ] - resource.user.item_operation.put: + resource.user.item_operation.put_custom: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false factory: [ "@api.operation_factory", "createItemOperation" ] - arguments: [ "@resource.user", "PUT" ] + arguments: + - "@resource.user" # Resource + - [ "PUT" ] # Methods + - "/users/{id}" # Path + - "partkeepr.user.put" # Controller + - "PartKeeprUserPut" + + resource.user.collection_operation.post_custom: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createCollectionOperation" ] + arguments: + - "@resource.user" # Resource + - [ "POST" ] # Methods + - "/users" # Path + - "partkeepr.user.post" # Controller + - "PartKeeprUserPost" resource.user.item_operation.delete: class: "Dunglas\ApiBundle\Api\Operation\Operation" @@ -1079,6 +1139,19 @@ services: factory: [ "@api.operation_factory", "createItemOperation" ] arguments: [ "@resource.user", "DELETE" ] + resource.user_provider: + parent: "api.resource" + arguments: [ "PartKeepr\AuthBundle\Entity\UserProvider" ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + resource.user: parent: "api.resource" arguments: [ "PartKeepr\AuthBundle\Entity\User" ] @@ -1088,12 +1161,26 @@ services: arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] - method: "initNormalizationContext" arguments: [ { groups: [ "default" ] } ] + - method: "initCollectionOperations" + arguments: [ [ "@resource.user.collection_operation.get", "@resource.user.collection_operation.post_custom", "@resource.user.collection_operation.get_providers" ] ] - method: "initItemOperations" - arguments: [ [ "@resource.user.item_operation.get", "@resource.user.item_operation.get", "@resource.tempfile.item_operation.put", "@resource.user.item_operation.delete", "@resource.user.item_operation.get_preferences", "@resource.user.item_operation.set_preference", "@resource.user.item_operation.login" ] ] + arguments: [ [ "@resource.user.item_operation.get", "@resource.user.item_operation.get", "@resource.user.item_operation.put_custom", "@resource.user.item_operation.delete", "@resource.user.item_operation.get_preferences", "@resource.user.item_operation.set_preference", "@resource.user.item_operation.delete_preference", "@resource.user.item_operation.login" ] ] - method: "initDenormalizationContext" arguments: - { groups: [ "default" ] } + resource.fos_user: + parent: "api.resource" + arguments: [ "PartKeepr\AuthBundle\Entity\FOSUser" ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } resource.project: parent: "api.resource" @@ -1218,3 +1305,4 @@ services: - method: "initDenormalizationContext" arguments: - { groups: [ "default" ] } + diff --git a/app/config/partkeepr.yml b/app/config/partkeepr.yml @@ -1,7 +1,7 @@ partkeepr: image_cache_directory: %kernel.cache_dir%/imagecache/ cronjob_check: false - authentication_provider: PartKeepr.Auth.WSSEAuthenticationProvider + authentication_provider: PartKeepr.Auth.HTTPBasicAuthenticationProvider directories: iclogo: %kernel.root_dir%/../data/images/iclogo/ temp: %kernel.root_dir%/../data/temp/ diff --git a/app/config/security.yml b/app/config/security.yml @@ -14,7 +14,7 @@ security: providers: chain_provider: chain: - providers: [in_memory, fos_userbundle] + providers: [in_memory, fos_userbundle, fr3d_ldapbundle] in_memory: memory: users: @@ -23,6 +23,8 @@ security: roles: 'ROLE_ADMIN' fos_userbundle: id: fos_user.user_provider.username + fr3d_ldapbundle: + id: fr3d_ldap.security.user.provider firewalls: login: @@ -32,6 +34,8 @@ security: stateless: true pattern: ^/api/.* provider: fos_userbundle + fr3d_ldap_httpbasic: + provider: fr3d_ldapbundle http_basic: provider: fos_userbundle wsse: diff --git a/composer.json b/composer.json @@ -28,6 +28,7 @@ ], "require": { "php": ">=5.6.0", + "fr3d/ldap-bundle": "2.0.*@dev", "symfony/symfony": "~2.7", "incenteev/composer-parameter-handler": "~2.0", "doctrine/orm": "~2.5", diff --git a/src/PartKeepr/AuthBundle/Action/DeletePreferenceAction.php b/src/PartKeepr/AuthBundle/Action/DeletePreferenceAction.php @@ -0,0 +1,59 @@ +<?php +namespace PartKeepr\AuthBundle\Action; + +use Dunglas\ApiBundle\Action\ActionUtilTrait; +use Dunglas\ApiBundle\Exception\RuntimeException; +use PartKeepr\AuthBundle\Services\UserPreferenceService; +use PartKeepr\AuthBundle\Services\UserService; +use PartKeepr\CategoryBundle\Exception\RootNodeNotFoundException; +use Symfony\Bridge\Doctrine\ManagerRegistry; +use Symfony\Component\HttpFoundation\Request; + +/** + * Returns the tree root node + */ +class DeletePreferenceAction +{ + use ActionUtilTrait; + + /** + * @var UserService + */ + private $userService; + + /** + * @var UserPreferenceService + */ + private $userPreferenceService; + + public function __construct( + UserService $userService, + UserPreferenceService $userPreferenceService + ) { + $this->userService = $userService; + $this->userPreferenceService = $userPreferenceService; + } + + /** + * Retrieves a collection of resources. + * + * @param Request $request + * + * @return array|\Dunglas\ApiBundle\Model\PaginatorInterface|\Traversable + * @throws \Exception If the format is invalid + * + * @throws RuntimeException|RootNodeNotFoundException + */ + public function __invoke(Request $request) + { + $user = $this->userService->getUser(); + + if ($request->request->has("preferenceKey")) { + $this->userPreferenceService->deletePreference($user, $request->request->get("preferenceKey")); + } else { + throw new \Exception("Invalid format"); + } + + return; + } +} diff --git a/src/PartKeepr/AuthBundle/Action/PostUserAction.php b/src/PartKeepr/AuthBundle/Action/PostUserAction.php @@ -0,0 +1,80 @@ +<?php +namespace PartKeepr\AuthBundle\Action; + + +use Dunglas\ApiBundle\Action\ActionUtilTrait; +use Dunglas\ApiBundle\Api\ResourceInterface; +use Dunglas\ApiBundle\Exception\RuntimeException; +use Dunglas\ApiBundle\Model\DataProviderInterface; +use PartKeepr\AuthBundle\Entity\User; +use PartKeepr\AuthBundle\Services\UserService; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Serializer\SerializerInterface; + +class PostUserAction +{ + use ActionUtilTrait; + + /** + * @var DataProviderInterface + */ + private $dataProvider; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var UserService + */ + private $userService; + + public function __construct( + DataProviderInterface $dataProvider, + SerializerInterface $serializer, + UserService $userService + ) { + $this->dataProvider = $dataProvider; + $this->serializer = $serializer; + $this->userService = $userService; + } + + /** + * Create a new item. + * + * @param Request $request + * + * @return mixed + * + * @throws NotFoundHttpException + * @throws RuntimeException + */ + public function __invoke(Request $request) + { + /** + * @var $resourceType ResourceInterface + */ + list($resourceType, $format) = $this->extractAttributes($request); + + /** + * @var User $data + */ + + $data = $this->serializer->deserialize( + $request->getContent(), + $resourceType->getEntityClass(), + $format, + $resourceType->getDenormalizationContext() + ); + + $data->setProvider($this->userService->getBuiltinProvider()); + + $this->userService->syncData($data); + + $data->setPassword(""); + + return $data; + } +} diff --git a/src/PartKeepr/AuthBundle/Action/PutUserAction.php b/src/PartKeepr/AuthBundle/Action/PutUserAction.php @@ -0,0 +1,83 @@ +<?php +namespace PartKeepr\AuthBundle\Action; + + +use Dunglas\ApiBundle\Action\ActionUtilTrait; +use Dunglas\ApiBundle\Api\ResourceInterface; +use Dunglas\ApiBundle\Exception\RuntimeException; +use Dunglas\ApiBundle\Model\DataProviderInterface; +use PartKeepr\AuthBundle\Entity\User; +use PartKeepr\AuthBundle\Services\UserService; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Serializer\SerializerInterface; + +class PutUserAction +{ + use ActionUtilTrait; + + /** + * @var DataProviderInterface + */ + private $dataProvider; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var UserService + */ + private $userService; + + public function __construct( + DataProviderInterface $dataProvider, + SerializerInterface $serializer, + UserService $userService + ) { + $this->dataProvider = $dataProvider; + $this->serializer = $serializer; + $this->userService = $userService; + } + + /** + * Create a new item. + * + * @param Request $request + * @param string|int $id + * + * @return mixed + * + * @throws NotFoundHttpException + * @throws RuntimeException + */ + public function __invoke(Request $request, $id) + { + /** + * @var $resourceType ResourceInterface + */ + list($resourceType, $format) = $this->extractAttributes($request); + + /** + * @var User $data + */ + $data = $this->getItem($this->dataProvider, $resourceType, $id); + + $context = $resourceType->getDenormalizationContext(); + $context['object_to_populate'] = $data; + + $data = $this->serializer->deserialize( + $request->getContent(), + $resourceType->getEntityClass(), + $format, + $context + ); + + $this->userService->syncData($data); + + $data->setPassword(""); + + return $data; + } +} diff --git a/src/PartKeepr/AuthBundle/Entity/FOSUser.php b/src/PartKeepr/AuthBundle/Entity/FOSUser.php @@ -3,10 +3,29 @@ namespace PartKeepr\AuthBundle\Entity; use Doctrine\ORM\Mapping as ORM; use FOS\UserBundle\Model\User as BaseUser; +use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; +use Symfony\Component\Serializer\Annotation\Groups; /** * @ORM\Entity * @ORM\Table(name="FOSUser") + * @ORM\AttributeOverrides({ + * @ORM\AttributeOverride(name="emailCanonical", + * column=@ORM\Column( + * name = "email_canonical", + * nullable = true, + * unique = false + * ) + * ), + * @ORM\AttributeOverride(name="email", + * column=@ORM\Column( + * name = "email", + * nullable = true, + * unique = false + * ) + * ) + * }) + * @TargetService(uri="/api/f_o_s_users") */ class FOSUser extends BaseUser { @@ -16,4 +35,10 @@ class FOSUser extends BaseUser * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; + + /** + * @var string + * @Groups({"default"}) + */ + protected $email; } diff --git a/src/PartKeepr/AuthBundle/Entity/User.php b/src/PartKeepr/AuthBundle/Entity/User.php @@ -5,12 +5,13 @@ use Doctrine\ORM\Mapping as ORM; use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; use PartKeepr\Util\BaseEntity; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; /** * @ORM\Entity * @ORM\Table( * name="PartKeeprUser", - * uniqueConstraints={@ORM\UniqueConstraint(name="username_provider", columns={"username", "provider"})}) + * uniqueConstraints={@ORM\UniqueConstraint(name="username_provider", columns={"username", "provider_id"})}) * @TargetService(uri="/api/users") */ class User extends BaseEntity @@ -27,6 +28,13 @@ class User extends BaseEntity private $password; /** + * @Assert\Email() + * @ORM\Column(length=255,nullable=true) + * @var string + */ + private $email; + + /** * @ORM\Column(type="boolean") */ private $admin; @@ -37,16 +45,21 @@ class User extends BaseEntity private $lastSeen; /** - * @ORM\Column(type="string", length=255) - * @var string + * @ORM\ManyToOne(targetEntity="PartKeepr\AuthBundle\Entity\UserProvider") + * @Groups({"default"}) */ private $provider; /** + * @ORM\OneToMany(targetEntity="PartKeepr\TipOfTheDayBundle\Entity\TipOfTheDayHistory", mappedBy="user", cascade={"remove"}, orphanRemoval=true) + */ + private $tipHistories; + + /** * Creates a new user object. * * @param string $username The username to set (optional) - * @param string $provider The authentification provider + * @param UserProvider $provider The authentification provider * * @throws \Exception */ @@ -56,29 +69,59 @@ class User extends BaseEntity $this->setUsername($username); } - if ($provider === null) { - throw new \Exception("The authentification provider must be specified"); + if ($provider !== null) { + $this->setProvider($provider); } - $this->setProvider($provider); - $this->setAdmin(false); } /** - * Sets the authentification provider + * Sets the admin flag * - * @param $provider + * @param boolean $bAdmin True if the user is an admin, false otherwise */ - public function setProvider($provider) + public function setAdmin($bAdmin) { - $this->provider = $provider; + $this->admin = (boolean)$bAdmin; + } + + /** + * @return mixed + */ + public function getTipHistories() + { + return $this->tipHistories; + } + + /** + * @param mixed $tipHistories + */ + public function setTipHistories($tipHistories) + { + $this->tipHistories = $tipHistories; + } + + /** + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * @param string $email + */ + public function setEmail($email) + { + $this->email = $email; } /** * Returns the authentification provider * - * @return string + * @return UserProvider */ public function getProvider() { @@ -86,13 +129,13 @@ class User extends BaseEntity } /** - * Sets the username. + * Sets the authentification provider * - * @param string $username The username to set. + * @param UserProvider $provider */ - public function setUsername($username) + public function setProvider(UserProvider $provider) { - $this->username = $username; + $this->provider = $provider; } /** @@ -119,13 +162,13 @@ class User extends BaseEntity } /** - * Sets the admin flag + * Sets the username. * - * @param boolean $bAdmin True if the user is an admin, false otherwise + * @param string $username The username to set. */ - public function setAdmin($bAdmin) + public function setUsername($username) { - $this->admin = (boolean)$bAdmin; + $this->username = $username; } /** @@ -139,37 +182,22 @@ class User extends BaseEntity } /** - * Sets the user's password. Automatically - * applies md5 hashing. - * - * @param string $password - */ - public function setPassword($password) - { - $this->setHashedPassword(md5($password)); - } - - /** - * Returns the user's md5-hashed password. - * - * @param none - * - * @return string The md5-hashed password + * @return string */ - public function getHashedPassword() + public function getPassword() { return $this->password; } /** - * Sets the user's password. Expects a hash - * and does not apply md5 hasing. + * Sets the user's password. * - * @param string $hashedPassword + * @Groups({"default"}) + * @param string $password */ - public function setHashedPassword($hashedPassword) + public function setPassword($password) { - $this->password = $hashedPassword; + $this->password = $password; } /** diff --git a/src/PartKeepr/AuthBundle/Entity/UserProvider.php b/src/PartKeepr/AuthBundle/Entity/UserProvider.php @@ -0,0 +1,69 @@ +<?php +namespace PartKeepr\AuthBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; +use PartKeepr\Util\BaseEntity; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + * @ORM\Table( + * name="UserProvider", + * uniqueConstraints={@ORM\UniqueConstraint(name="type", columns={"type"})}) + * @TargetService(uri="/api/user_providers") + */ +class UserProvider extends BaseEntity +{ + /** + * @ORM\Column(type="string", length=255) + * @Groups({"default"}) + * @var string + */ + private $type; + + /** + * @ORM\Column(type="boolean") + * @Groups({"default"}) + * @var bool + */ + private $editable; + + public function __construct() + { + $this->setEditable(true); + } + + /** + * @return mixed + */ + public function isEditable() + { + return $this->editable; + } + + /** + * @param mixed $editable + */ + public function setEditable($editable) + { + $this->editable = $editable; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $type + */ + public function setType($type) + { + $this->type = $type; + } + +} diff --git a/src/PartKeepr/AuthBundle/Resources/config/actions.xml b/src/PartKeepr/AuthBundle/Resources/config/actions.xml @@ -5,16 +5,32 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <service id="partkeepr.user_preference.get_preferences" class="PartKeepr\AuthBundle\Action\GetPreferencesAction"> + <service id="partkeepr.user_preference.get_preferences" + class="PartKeepr\AuthBundle\Action\GetPreferencesAction"> <argument type="service" id="partkeepr.userservice"/> <argument type="service" id="partkeepr.user_preference_service"/> <argument type="service" id="api.serializer"/> </service> - <service id="partkeepr.user_preference.set_preference" class="PartKeepr\AuthBundle\Action\SetPreferenceAction"> + <service id="partkeepr.user_preference.set_preference" class="PartKeepr\AuthBundle\Action\SetPreferenceAction"> <argument type="service" id="partkeepr.userservice"/> <argument type="service" id="partkeepr.user_preference_service"/> <argument type="service" id="api.serializer"/> </service> + <service id="partkeepr.user_preference.delete_preference" + class="PartKeepr\AuthBundle\Action\DeletePreferenceAction"> + <argument type="service" id="partkeepr.userservice"/> + <argument type="service" id="partkeepr.user_preference_service"/> + </service> + <service id="partkeepr.user.put" class="PartKeepr\AuthBundle\Action\PutUserAction"> + <argument type="service" id="api.data_provider"/> + <argument type="service" id="api.serializer"/> + <argument type="service" id="partkeepr.userservice"/> + </service> + <service id="partkeepr.user.post" class="PartKeepr\AuthBundle\Action\PostUserAction"> + <argument type="service" id="api.data_provider"/> + <argument type="service" id="api.serializer"/> + <argument type="service" id="partkeepr.userservice"/> + </service> <service id="partkeepr.auth.login" class="PartKeepr\AuthBundle\Action\LoginAction"> <argument type="service" id="partkeepr.userservice"/> <argument type="service" id="api.serializer"/> diff --git a/src/PartKeepr/AuthBundle/Resources/config/services.xml b/src/PartKeepr/AuthBundle/Resources/config/services.xml @@ -13,6 +13,8 @@ <service id="partkeepr.userservice" class="PartKeepr\AuthBundle\Services\UserService"> <argument type="service" id="security.token_storage"/> <argument type="service" id="doctrine.orm.entity_manager"/> + <argument type="service" id="fos_user.util.user_manipulator"/> + <argument type="service" id="fos_user.user_manager"/> </service> <service id="partkeepr.user_preference_service" class="PartKeepr\AuthBundle\Services\UserPreferenceService"> <argument type="service" id="doctrine.orm.entity_manager"/> diff --git a/src/PartKeepr/AuthBundle/Security/Authentication/AuthenticationProviderManager.php b/src/PartKeepr/AuthBundle/Security/Authentication/AuthenticationProviderManager.php @@ -24,6 +24,9 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface private $eraseCredentials; + /** + * @var EventDispatcherInterface + */ private $eventDispatcher; /** @@ -42,7 +45,7 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface foreach ($providers as $provider) { if (!$provider instanceof AuthenticationProviderInterface) { - throw new \InvalidArgumentException(sprintf('Provider "%s" must implement the AuthenticationProviderInterface.', + throw new \InvalidArgumentException(sprintf('UserProvider "%s" must implement the AuthenticationProviderInterface.', get_class($provider))); } } @@ -73,7 +76,7 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface $result = $provider->authenticate($token); if (null !== $result) { - $result->setAttribute("provider", get_class($provider)); + $result->setAttribute("provider", $provider); break; } } catch (AccountStatusException $e) { @@ -99,7 +102,7 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface } if (null === $lastException) { - $lastException = new ProviderNotFoundException(sprintf('No Authentication Provider found for token of class "%s".', + $lastException = new ProviderNotFoundException(sprintf('No Authentication UserProvider found for token of class "%s".', get_class($token))); } diff --git a/src/PartKeepr/AuthBundle/Services/UserService.php b/src/PartKeepr/AuthBundle/Services/UserService.php @@ -5,11 +5,18 @@ namespace PartKeepr\AuthBundle\Services; use Doctrine\ORM\EntityManager; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; +use FOS\UserBundle\Model\UserManagerInterface; +use FOS\UserBundle\Util\UserManipulator; use PartKeepr\AuthBundle\Entity\User; +use PartKeepr\AuthBundle\Entity\UserProvider; +use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; class UserService { + /** + * @var TokenStorage + */ private $tokenStorage; /** @@ -17,20 +24,120 @@ class UserService */ private $entityManager; - public function __construct(TokenStorage $tokenStorage, EntityManager $entityManager) - { + /** + * @var UserManipulator + */ + private $userManipulator; + + /** + * @var UserManagerInterface + */ + private $userManager; + + const BUILTIN_PROVIDER = "Builtin"; + + public function __construct( + TokenStorage $tokenStorage, + EntityManager $entityManager, + UserManipulator $userManipulator, + UserManagerInterface $userManager + ) { $this->tokenStorage = $tokenStorage; $this->entityManager = $entityManager; + $this->userManipulator = $userManipulator; + $this->userManager = $userManager; } public function getUser() { - $provider = $this->tokenStorage->getToken()->getAttribute("provider"); + $tokenProvider = $this->tokenStorage->getToken()->getAttribute("provider"); + + $provider = $this->getProvider($tokenProvider); $username = $this->tokenStorage->getToken()->getUsername(); return $this->getProxyUser($username, $provider); } + public function getProvider(AuthenticationProviderInterface $authenticationProvider) + { + $providerClass = get_class($authenticationProvider); + $providerType = $this->getProviderTypeByClass($providerClass); + + return $this->getProviderByType($providerType); + } + + public function getProviderTypeByClass($providerClass) + { + switch ($providerClass) { + case 'Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Provider': + return self::BUILTIN_PROVIDER; + break; + case 'Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider': + return self::BUILTIN_PROVIDER; + break; + case 'FR3D\LdapBundle\Security\Authentication\LdapAuthenticationProvider': + return "LDAP"; + break; + default: + throw new \Exception("Unknown provider ".$providerClass); + } + } + + /** + * Syncronizes the data of the given user with the FOSRestBundle + * @throws \Exception If the password was not set + * @param $user + */ + public function syncData(User $user) + { + if ($user->getProvider()->getType() !== self::BUILTIN_PROVIDER) { + return; + } + + $FOSUser = $this->userManager->findUserByUsername($user->getUsername()); + + if ($FOSUser === null) { + + if ($user->getPassword() == "") { + throw new \Exception("Password must be set"); + } + + $FOSUser = $this->userManipulator->create($user->getUsername(), $user->getPassword(), "", true, false); + } + if ($user->getPassword() != "") { + $this->userManipulator->changePassword($user->getUsername(), $user->getPassword()); + } + + + $FOSUser->setEmail($user->getEmail()); + } + + public function getProviderByType($type) + { + $provider = $this->entityManager->getRepository("PartKeeprAuthBundle:UserProvider")->findOneBy(array("type" => $type)); + + if ($provider !== null) { + return $provider; + } + + $provider = new UserProvider(); + $provider->setType($type); + + if ($type !== self::BUILTIN_PROVIDER) { + $provider->setEditable(false); + } + + $this->entityManager->persist($provider); + $this->entityManager->flush(); + + return $provider; + } + + public function getBuiltinProvider() + { + return $this->getProviderByType(self::BUILTIN_PROVIDER); + } + public function getProxyUser($username, $provider) { /** diff --git a/src/PartKeepr/AuthBundle/Tests/Services/UserPreferenceServiceTest.php b/src/PartKeepr/AuthBundle/Tests/Services/UserPreferenceServiceTest.php @@ -9,11 +9,12 @@ class UserPreferenceServiceTest extends WebTestCase public function testBasics() { $service = $this->getContainer()->get("partkeepr.user_preference_service"); + $userService = $this->getContainer()->get("partkeepr.userservice"); /** * @var User $user */ - $user = $this->getContainer()->get("partkeepr.userservice")->getProxyUser("admin", "test"); + $user = $userService->getProxyUser("admin", $userService->getBuiltinProvider()); $service->setPreference($user, "foo", "bar"); $this->assertEquals("bar", $service->getPreferenceValue($user, "foo")); @@ -48,7 +49,9 @@ class UserPreferenceServiceTest extends WebTestCase public function testGetPreferencesException() { $service = $this->getContainer()->get("partkeepr.user_preference_service"); - $user = new User("admin", "test"); + $userService = $this->getContainer()->get("partkeepr.userservice"); + + $user = new User("admin", $userService->getBuiltinProvider()); $this->setExpectedException("PartKeepr\Util\Exceptions\EntityNotPersistantException"); $service->getPreferences($user); } @@ -56,7 +59,8 @@ class UserPreferenceServiceTest extends WebTestCase public function testGetPreferenceUserException() { $service = $this->getContainer()->get("partkeepr.user_preference_service"); - $user = new User("admin", "test"); + $userService = $this->getContainer()->get("partkeepr.userservice"); + $user = new User("admin", $userService->getBuiltinProvider()); $this->setExpectedException("PartKeepr\Util\Exceptions\EntityNotPersistantException"); $service->getPreference($user, "BLA"); } @@ -64,11 +68,12 @@ class UserPreferenceServiceTest extends WebTestCase public function testGetPreferenceException() { $service = $this->getContainer()->get("partkeepr.user_preference_service"); + $userService = $this->getContainer()->get("partkeepr.userservice"); /** * @var User $user */ - $user = $this->getContainer()->get("partkeepr.userservice")->getProxyUser("admin", "test"); + $user = $this->getContainer()->get("partkeepr.userservice")->getProxyUser("admin", $userService->getBuiltinProvider()); $this->setExpectedException("PartKeepr\AuthBundle\Exceptions\UserPreferenceNotFoundException"); $service->getPreference($user, "BLA"); @@ -77,7 +82,9 @@ class UserPreferenceServiceTest extends WebTestCase public function testSetPreferenceException() { $service = $this->getContainer()->get("partkeepr.user_preference_service"); - $user = new User("admin", "test"); + $userService = $this->getContainer()->get("partkeepr.userservice"); + + $user = new User("admin", $userService->getBuiltinProvider()); $this->setExpectedException("PartKeepr\Util\Exceptions\EntityNotPersistantException"); $service->setPreference($user, "FOO", "BAR"); } @@ -85,7 +92,9 @@ class UserPreferenceServiceTest extends WebTestCase public function testDeletePreferenceException() { $service = $this->getContainer()->get("partkeepr.user_preference_service"); - $user = new User("admin", "test"); + $userService = $this->getContainer()->get("partkeepr.userservice"); + + $user = new User("admin", $userService->getBuiltinProvider()); $this->setExpectedException("PartKeepr\Util\Exceptions\EntityNotPersistantException"); $service->deletePreference($user, "FOO"); } diff --git a/src/PartKeepr/CoreBundle/DependencyInjection/Configuration.php b/src/PartKeepr/CoreBundle/DependencyInjection/Configuration.php @@ -19,7 +19,7 @@ class Configuration implements ConfigurationInterface ->scalarNode('authentication_provider') ->cannotBeEmpty() ->defaultValue('PartKeepr.Auth.WSSEAuthenticationProvider') - ->info('The authentication provider for the frontend') + ->info('The authentication provider for the frontend. Available choices: PartKeepr.Auth.WSSEAuthenticationProvider and PartKeepr.Auth.HTTPBasicAuthenticationProvider') ->end() ->scalarNode('tip_of_the_day_uri') ->cannotBeEmpty() @@ -41,15 +41,15 @@ class Configuration implements ConfigurationInterface ->isRequired() ->info('The image cache directory') ->end() - ->arrayNode('directories') - ->prototype('scalar') + ->arrayNode('directories') + ->prototype('scalar') ->end() ->end() ->booleanNode('cronjob_check') ->defaultTrue() ->info('Whether the system should check if cronjobs are running or not') - ->end() - ->end(); + ->end() + ->end(); return $treeBuilder; } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorComponent.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorComponent.js @@ -126,11 +126,6 @@ Ext.define('PartKeepr.EditorComponent', { editor = this.createEditor(record.get(this.titleProperty)); editor.editItem(record); this.editorTabPanel.add(editor).show(); - }, - failure: function (record, operation) - { - console.log(record); - console.log(operation); } }); }, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Statusbar.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Statusbar.js @@ -67,7 +67,7 @@ Ext.define('PartKeepr.Statusbar', { { var user = PartKeepr.getApplication().getLoginManager().getUser(); - this.setCurrentUser(user.username); + this.setCurrentUser(user.get("username")); this.connectionButton.setConnected(); }, setDisconnected: function () diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserEditor.js @@ -1,42 +1,30 @@ Ext.define('PartKeepr.UserEditor', { - extend: 'PartKeepr.Editor', - alias: 'widget.UserEditor', - - saveText: i18n("Save User"), - model: 'PartKeepr.User', - - initComponent: function () { - this.gridPanel = Ext.create("PartKeepr.UserPreferenceGrid"); - - var container = Ext.create("Ext.form.FieldContainer", { - fieldLabel: i18n("User Preferences"), - labelWidth: 150, - layout: 'fit', - height: 200, - items: this.gridPanel - }); - - this.items = [{ - xtype: 'textfield', - name: 'username', - fieldLabel: i18n("User") - },{ - xtype: 'textfield', - inputType: "password", - name: 'password', - fieldLabel: i18n("Password") - }, container ]; - - this.on("startEdit", this.onStartEdit, this); - this.callParent(); - }, - onStartEdit: function () { - this.gridPanel.store.getProxy().extraParams.user_id = this.record.get("id"); - this.gridPanel.store.load(); - }, - onItemSave: function () { - this.gridPanel.syncPreferences(); - - this.callParent(); - } -}); + extend: 'PartKeepr.Editor', + alias: 'widget.UserEditor', + + saveText: i18n("Save User"), + model: 'PartKeepr.User', + titleProperty: 'username', + + initComponent: function () + { + this.items = [ + { + xtype: 'textfield', + name: 'username', + fieldLabel: i18n("User") + }, { + xtype: 'textfield', + name: 'email', + vtype: 'email', + fieldLabel: i18n("E-Mail") + }, { + xtype: 'textfield', + inputType: "password", + name: 'password', + fieldLabel: i18n("Password") + } + ]; + + this.callParent(); + }}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserEditorComponent.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserEditorComponent.js @@ -1,22 +1,28 @@ Ext.define('PartKeepr.UserEditorComponent', { - extend: 'PartKeepr.EditorComponent', - alias: 'widget.UserEditorComponent', - navigationClass: 'PartKeepr.UserGrid', - editorClass: 'PartKeepr.UserEditor', - newItemText: i18n("New User"), - deleteMessage: i18n("Do you really wish to delete the user '%s'?"), - deleteTitle: i18n("Delete User"), - - model: 'PartKeepr.User', - - initComponent: function () { - this.createStore({ - sorters: [{ - property: 'username', - direction:'ASC' - }] - }); - - this.callParent(); - } -});- \ No newline at end of file + extend: 'PartKeepr.EditorComponent', + alias: 'widget.UserEditorComponent', + navigationClass: 'PartKeepr.UserGrid', + editorClass: 'PartKeepr.UserEditor', + newItemText: i18n("New User"), + deleteMessage: i18n("Do you really wish to delete the user '%s'?"), + deleteTitle: i18n("Delete User"), + + model: 'PartKeepr.AuthBundle.Entity.User', + + titleProperty: 'username', + + initComponent: function () + { + this.createStore({ + sorters: [ + { + property: 'username', + direction: 'ASC' + } + ], + autoLoad: false + }); + + this.callParent(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserGrid.js @@ -1,12 +1,63 @@ Ext.define('PartKeepr.UserGrid', { - extend: 'PartKeepr.EditorGrid', - alias: 'widget.UserGrid', - columns: [ - {header: i18n("User"), dataIndex: 'username', flex: 1} - ], - addButtonText: i18n("Add User"), - addButtonIcon: 'resources/silkicons/user_add.png', + extend: 'PartKeepr.EditorGrid', + alias: 'widget.UserGrid', + columns: [ + {header: i18n("User"), dataIndex: 'username', flex: 1}, + { + header: i18n("Provider"), renderer: function (value, metaData, record) + { + if (record.getProvider() !== null) { + return record.getProvider().get("type"); + } else { + return ""; + } + + + }, flex: 1 + } + ], + addButtonText: i18n("Add User"), + addButtonIconCls: 'web-icon user_add', deleteButtonText: i18n("Delete User"), - deleteButtonIcon: 'resources/silkicons/user_delete.png', - automaticPageSize: true -});- \ No newline at end of file + deleteButtonIconCls: 'web-icon user_delete', + automaticPageSize: true, + + initComponent: function () + { + this.callParent(arguments); + + this.providerStore = Ext.create("PartKeepr.data.store.UserProviderStore"); + + this.providerCombo = Ext.create("Ext.form.field.ComboBox", { + store: this.providerStore, + displayField: 'type', + valueField: '@Id', + editable: false, + forceSelection: true, + fieldLabel: i18n("Type"), + listeners: { + select: "onProviderSelect", + scope: this + } + }); + + this.providerToolbar = Ext.create("Ext.toolbar.Toolbar", { + dock: 'top', + enableOverflow: true, + items: this.providerCombo + }); + + this.filter = Ext.create("Ext.util.Filter", { + property: "provider", + operator: "=", + value: "" + }); + + this.addDocked(this.providerToolbar); + }, + onProviderSelect: function (combo, record) + { + this.filter.setValue(record); + this.store.addFilter(this.filter); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/UserPreferenceGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/UserPreferenceGrid.js @@ -6,13 +6,13 @@ Ext.define('PartKeepr.UserPreferenceGrid', { columns: [ { header: i18n("Key"), - dataIndex: 'key', + dataIndex: 'preferenceKey', flex: 0.3, minWidth: 200, renderer: Ext.util.Format.htmlEncode }, { header: i18n("Value"), - dataIndex: 'value', + dataIndex: 'preferenceValue', flex: 0.7, minWidth: 200, renderer: Ext.util.Format.htmlEncode @@ -27,7 +27,7 @@ Ext.define('PartKeepr.UserPreferenceGrid', { disabled: true, itemId: 'delete', scope: this, - icon: 'resources/silkicons/delete.png', + iconCls: 'web-icon delete', handler: this.onDeleteClick }); @@ -41,6 +41,7 @@ Ext.define('PartKeepr.UserPreferenceGrid', { ]; this.store = Ext.create("Ext.data.Store", { model: 'PartKeepr.AuthBundle.Entity.UserPreference', + remoteFilter: true, pageSize: 999999999 }); @@ -51,29 +52,19 @@ Ext.define('PartKeepr.UserPreferenceGrid', { onDeleteClick: function () { var selection = this.getView().getSelectionModel().getSelection()[0]; + if (selection) { - // Set phantom to false because ExtJS has problems with PK-less thingies - selection.phantom = false; - this.store.remove(selection); + selection.getProxy().callCollectionAction(null, "DELETE", { + "preferenceKey": selection.get("preferenceKey") + }, Ext.bind(this.onPreferenceDeleted, this)); } }, - onSelectChange: function (selModel, selections) + onPreferenceDeleted: function () { - this.deleteButton.setDisabled(selections.length === 0); + this.store.load(); }, - syncPreferences: function () + onSelectChange: function (selModel, selections) { - /* Iterate through all removed records and issue an AJAX - * call. This is necessary because the server side doesn't suport string - * keys and ExtJS can't handle composite keys. - */ - for (var j = 0; j < this.store.removed.length; j++) { - var call = new PartKeepr.ServiceCall("UserPreference", "destroy"); - call.setParameter("key", this.store.removed[j].get("key")); - call.setParameter("user_id", this.store.removed[j].get("user_id")); - call.doCall(); - } - - this.store.removed = []; + this.deleteButton.setDisabled(selections.length === 0); } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js @@ -92,7 +92,13 @@ Ext.define("PartKeepr.data.HydraProxy", { */ callAction: function (record, action, method, parameters, callback, reload) { - var url = record.getId() + "/" + action; + var url; + + if (action !== null) { + url = record.getId() + "/" + action; + } else { + url = record.getId(); + } var request = Ext.create("Ext.data.Request"); request.setMethod(method); @@ -127,23 +133,26 @@ Ext.define("PartKeepr.data.HydraProxy", { * @param {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects * @return {String} The encoded filters */ - encodeFilters: function(filters) { + encodeFilters: function (filters) + { var out = [], length = filters.length, - i, filter,j; + i, filter, j; for (i = 0; i < length; i++) { filter = filters[i].serialize(); - if (Object.prototype.toString.call( filter.value ) === '[object Array]') { - for (j = 0;j<filter.value.length;j++) { + if (Object.prototype.toString.call(filter.value) === '[object Array]') { + for (j = 0; j < filter.value.length; j++) { if (filter.value[j].isModel && filter.value[j].isModel === true) { - filter.value[j] = filter.value[j].getId(); - } + filter.value[j] = filter.value[j].getId(); + } } - } else if (typeof filter.value == "object") { - if (filter.value.isModel && filter.value.isModel === true) { - filter.value = filter.value.getId(); + } else { + if (typeof filter.value == "object") { + if (filter.value.isModel && filter.value.isModel === true) { + filter.value = filter.value.getId(); + } } } out[i] = filter; @@ -159,7 +168,14 @@ Ext.define("PartKeepr.data.HydraProxy", { */ callCollectionAction: function (action, method, parameters, callback, ignoreException) { - var url = this.url + "/" + action; + var url; + + if (action !== null) { + url = this.url + "/" + action; + } else { + url = this.url; + } + var request = Ext.create("Ext.data.Request"); request.setMethod(method); @@ -187,7 +203,6 @@ Ext.define("PartKeepr.data.HydraProxy", { return; } - console.log(ignoreException); if (!ignoreException) { this.showException(response); } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/UserProvidersStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/UserProvidersStore.js @@ -0,0 +1,20 @@ +Ext.define('PartKeepr.data.store.UserProviderStore', { + extend: 'Ext.data.Store', + + /** + * The store ID to use + */ + storeId: 'UserProviderStore', + + /** + * Automatically load the store + */ + autoLoad: true, + + /** + * The model to use + */ + model: "PartKeepr.AuthBundle.Entity.UserProvider", + + pageSize: 99999999 +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig @@ -59,6 +59,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/Auth/WSSEAuthenticationProvider.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/TipOfTheDayStore.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/TipOfTheDayHistoryStore.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/UserProvidersStore.js' '@PartKeeprFrontendBundle/Resources/public/js/Models/ProjectReport.js' '@PartKeeprFrontendBundle/Resources/public/js/Models/ProjectReportList.js' '@PartKeeprFrontendBundle/Resources/public/js/Models/SystemInformationRecord.js'