diff --git a/.gitignore b/.gitignore index ed249de0..e265463b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ ###> phpunit/phpunit ### /phpunit.xml +/.phpunit.result.cache ###< phpunit/phpunit ### ###> friendsofphp/php-cs-fixer ### diff --git a/composer.json b/composer.json index 6bda480b..8475301c 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,11 @@ "ext-pdo_sqlite": "*", "beberlei/DoctrineExtensions": "^1.0", "erusev/parsedown": "^1.6", + "friendsofsymfony/rest-bundle": "^2.3", "friendsofsymfony/user-bundle": "~2.0", + "jms/serializer-bundle": "^2.4", "kevinpapst/adminlte-bundle": "^1.2", + "nelmio/api-doc-bundle": "^3.2", "ocramius/proxy-manager": "2.1.1", "sensio/framework-extra-bundle": "^5.1", "symfony/asset": "^4.0", @@ -92,7 +95,7 @@ "extra": { "symfony": { "id": "01C3FWRDJJEX9K6Y3A4XDFXPBR", - "allow-contrib": false + "allow-contrib": true } }, "repositories": [ diff --git a/composer.lock b/composer.lock index dc14be69..20175aff 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f2eb14bbc5ce80848484f44ae4639678", + "content-hash": "c5204d1dd48134e0b9ed8a2c0033dc05", "packages": [ { "name": "beberlei/DoctrineExtensions", @@ -1326,6 +1326,147 @@ ], "time": "2018-03-08T01:11:30+00:00" }, + { + "name": "exsyst/swagger", + "version": "v0.4.1", + "source": { + "type": "git", + "url": "https://github.com/GuilhemN/swagger.git", + "reference": "a02984db5edacdce2b4e09dae5ba8fe17a0e449e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GuilhemN/swagger/zipball/a02984db5edacdce2b4e09dae5ba8fe17a0e449e", + "reference": "a02984db5edacdce2b4e09dae5ba8fe17a0e449e", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "EXSyst\\Component\\Swagger\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ener-Getick", + "email": "egetick@gmail.com" + } + ], + "description": "A php library to manipulate Swagger specifications", + "time": "2018-07-27T06:40:00+00:00" + }, + { + "name": "friendsofsymfony/rest-bundle", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git", + "reference": "1abdf3d82502ac67b93c7f84c844fa147f0ec70e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/1abdf3d82502ac67b93c7f84c844fa147f0ec70e", + "reference": "1abdf3d82502ac67b93c7f84c844fa147f0ec70e", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.0", + "php": "^5.5.9|~7.0", + "psr/log": "^1.0", + "symfony/config": "^2.7|^3.0|^4.0", + "symfony/debug": "^2.7|^3.0|^4.0", + "symfony/dependency-injection": "^2.7|^3.0|^4.0", + "symfony/event-dispatcher": "^2.7|^3.0|^4.0", + "symfony/finder": "^2.7|^3.0|^4.0", + "symfony/framework-bundle": "^2.7|^3.0|^4.0", + "symfony/http-foundation": "^2.7|^3.0|^4.0", + "symfony/http-kernel": "^2.7|^3.0|^4.0", + "symfony/routing": "^2.7|^3.0|^4.0", + "symfony/security-core": "^2.7|^3.0|^4.0", + "symfony/templating": "^2.7|^3.0|^4.0", + "willdurand/jsonp-callback-validator": "^1.0", + "willdurand/negotiation": "^2.0" + }, + "conflict": { + "jms/serializer": "1.3.0", + "jms/serializer-bundle": "<1.2.0", + "sensio/framework-extra-bundle": "<3.0.13" + }, + "require-dev": { + "jms/serializer-bundle": "^1.2|^2.0", + "phpoption/phpoption": "^1.1", + "psr/http-message": "^1.0", + "sensio/framework-extra-bundle": "^3.0.13|^4.0|^5.0", + "symfony/asset": "^2.7|^3.0|^4.0", + "symfony/browser-kit": "^2.7|^3.0|^4.0", + "symfony/css-selector": "^2.7|^3.0|^4.0", + "symfony/dependency-injection": "^2.7|^3.0|^4.0", + "symfony/expression-language": "~2.7|^3.0|^4.0", + "symfony/form": "^2.7|^3.0|^4.0", + "symfony/phpunit-bridge": "^3.2|^4.0", + "symfony/security-bundle": "^2.7|^3.0|^4.0", + "symfony/serializer": "^2.7.11|^3.0.4|^4.0", + "symfony/twig-bundle": "^2.7|^3.0|^4.0", + "symfony/validator": "^2.7|^3.0|^4.0", + "symfony/web-profiler-bundle": "^2.7|^3.0|^4.0", + "symfony/yaml": "^2.7|^3.0|^4.0" + }, + "suggest": { + "jms/serializer-bundle": "Add support for advanced serialization capabilities, recommended, requires ^1.0", + "sensio/framework-extra-bundle": "Add support for route annotations and the view response listener, requires ^3.0", + "symfony/expression-language": "Add support for using the expression language in the routing, requires ^2.7|^3.0", + "symfony/serializer": "Add support for basic serialization capabilities and xml decoding, requires ^2.7|^3.0", + "symfony/validator": "Add support for validation capabilities in the ParamFetcher, requires ^2.7|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "FOS\\RestBundle\\": "" + }, + "exclude-from-classmap": [ + "Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukas Kahwe Smith", + "email": "smith@pooteeweet.org" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/friendsofsymfony/FOSRestBundle/contributors" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + } + ], + "description": "This Bundle provides various tools to rapidly develop RESTful API's with Symfony", + "homepage": "http://friendsofsymfony.github.com", + "keywords": [ + "rest" + ], + "time": "2018-02-28T13:57:04+00:00" + }, { "name": "friendsofsymfony/user-bundle", "version": "v2.1.2", @@ -1452,6 +1593,250 @@ ], "time": "2014-01-12T16:20:24+00:00" }, + { + "name": "jms/metadata", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "time": "2016-12-05T10:18:33+00:00" + }, + { + "name": "jms/parser-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/parser-lib.git", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "shasum": "" + }, + "require": { + "phpoption/phpoption": ">=0.9,<2.0-dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "description": "A library for easily creating recursive-descent parsers.", + "time": "2012-11-18T18:08:43+00:00" + }, + { + "name": "jms/serializer", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "00863e1d55b411cc33ad3e1de09a4c8d3aae793c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/00863e1d55b411cc33ad3e1de09a4c8d3aae793c", + "reference": "00863e1d55b411cc33ad3e1de09a4c8d3aae793c", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/instantiator": "^1.0.3", + "jms/metadata": "^1.3", + "jms/parser-lib": "1.*", + "php": "^5.5|^7.0", + "phpcollection/phpcollection": "~0.1", + "phpoption/phpoption": "^1.1" + }, + "conflict": { + "twig/twig": "<1.12" + }, + "require-dev": { + "doctrine/orm": "~2.1", + "doctrine/phpcr-odm": "^1.3|^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "phpunit/phpunit": "^4.8|^5.0", + "propel/propel1": "~1.7", + "psr/container": "^1.0", + "symfony/dependency-injection": "^2.7|^3.3|^4.0", + "symfony/expression-language": "^2.6|^3.0", + "symfony/filesystem": "^2.1", + "symfony/form": "~2.1|^3.0", + "symfony/translation": "^2.1|^3.0", + "symfony/validator": "^2.2|^3.0", + "symfony/yaml": "^2.1|^3.0", + "twig/twig": "~1.12|~2.0" + }, + "suggest": { + "doctrine/cache": "Required if you like to use cache functionality.", + "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", + "symfony/yaml": "Required if you'd like to serialize data to YAML format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.13-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\Serializer": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "time": "2018-07-25T13:58:54+00:00" + }, + { + "name": "jms/serializer-bundle", + "version": "2.4.2", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/JMSSerializerBundle.git", + "reference": "22eb9a2f7983394b0770237ca91e879eb2a4b4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/JMSSerializerBundle/zipball/22eb9a2f7983394b0770237ca91e879eb2a4b4a6", + "reference": "22eb9a2f7983394b0770237ca91e879eb2a4b4a6", + "shasum": "" + }, + "require": { + "jms/serializer": "^1.10", + "php": "^5.4|^7.0", + "phpoption/phpoption": "^1.1.0", + "symfony/framework-bundle": "~2.3|~3.0|~4.0" + }, + "require-dev": { + "doctrine/orm": "*", + "phpunit/phpunit": "^4.8.35|^5.4.3|^6.0", + "symfony/expression-language": "~2.6|~3.0|~4.0", + "symfony/finder": "^2.3|^3.0|^4.0", + "symfony/form": "*", + "symfony/stopwatch": "*", + "symfony/twig-bundle": "*", + "symfony/validator": "*", + "symfony/yaml": "*" + }, + "suggest": { + "jms/di-extra-bundle": "Required to get lazy loading (de)serialization visitors, ~1.3", + "symfony/finder": "Required for cache warmup, supported versions ^2.3|^3.0|^4.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "JMS\\SerializerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Allows you to easily serialize, and deserialize data of any complexity", + "homepage": "http://jmsyst.com/bundles/JMSSerializerBundle", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "time": "2018-06-19T13:39:25+00:00" + }, { "name": "kevinpapst/adminlte-bundle", "version": "1.2", @@ -1590,6 +1975,88 @@ ], "time": "2017-06-19T01:22:40+00:00" }, + { + "name": "nelmio/api-doc-bundle", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioApiDocBundle.git", + "reference": "2c72aa4efff07862514aaf2cf078fabc58c465d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/2c72aa4efff07862514aaf2cf078fabc58c465d6", + "reference": "2c72aa4efff07862514aaf2cf078fabc58c465d6", + "shasum": "" + }, + "require": { + "exsyst/swagger": "~0.3|~0.4", + "php": "^7.0", + "phpdocumentor/reflection-docblock": "^3.1|^4.0", + "symfony/framework-bundle": "^3.4|^4.0", + "symfony/options-resolver": "^3.4.4|^4.0", + "symfony/property-info": "^3.4|^4.0", + "zircote/swagger-php": "^2.0.9" + }, + "require-dev": { + "api-platform/core": "^2.1.0", + "doctrine/annotations": "^1.2", + "doctrine/common": "^2.4", + "friendsofsymfony/rest-bundle": "^2.0", + "jms/serializer-bundle": "^2.0", + "sensio/framework-extra-bundle": "^3.0", + "symfony/asset": "^3.4|^4.0", + "symfony/browser-kit": "^3.4|^4.0", + "symfony/cache": "^3.4|^4.0", + "symfony/config": "^3.4|^4.0", + "symfony/console": "^3.4|^4.0", + "symfony/dom-crawler": "^3.4|^4.0", + "symfony/form": "^3.4|^4.0", + "symfony/phpunit-bridge": "^3.4", + "symfony/property-access": "^3.4|^4.0", + "symfony/stopwatch": "^3.4|^4.0", + "symfony/templating": "^3.4|^4.0", + "symfony/twig-bundle": "^3.4|^4.0", + "symfony/validator": "^3.4|^4.0" + }, + "suggest": { + "api-platform/core": "For using an API oriented framework.", + "friendsofsymfony/rest-bundle": "For using the parameters annotations." + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\ApiDocBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioApiDocBundle/contributors" + } + ], + "description": "Generates documentation for your REST API from annotations", + "keywords": [ + "api", + "doc", + "documentation", + "rest" + ], + "time": "2018-04-27T17:36:50+00:00" + }, { "name": "ocramius/package-versions", "version": "1.3.0", @@ -1826,6 +2293,256 @@ ], "time": "2018-07-04T16:31:37+00:00" }, + { + "name": "phpcollection/phpcollection", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-collection.git", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "shasum": "" + }, + "require": { + "phpoption/phpoption": "1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4-dev" + } + }, + "autoload": { + "psr-0": { + "PhpCollection": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "General-Purpose Collection Library for PHP", + "keywords": [ + "collection", + "list", + "map", + "sequence", + "set" + ], + "time": "2015-05-17T12:39:23+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2017-09-11T18:02:19+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-11-30T07:14:17+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14T14:27:02+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed", + "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-0": { + "PhpOption\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "time": "2015-07-25T16:39:46+00:00" + }, { "name": "psr/cache", "version": "1.0.1", @@ -4014,6 +4731,82 @@ ], "time": "2018-07-26T09:10:45+00:00" }, + { + "name": "symfony/property-info", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "807e103da26f1ea0256e587fe918fe2ba68989fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/807e103da26f1ea0256e587fe918fe2ba68989fb", + "reference": "807e103da26f1ea0256e587fe918fe2ba68989fb", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/inflector": "~3.4|~4.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.2", + "phpdocumentor/type-resolver": "<0.3.0", + "symfony/dependency-injection": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0", + "symfony/cache": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/serializer": "~3.4|~4.0" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "To use the PHPDoc", + "psr/cache-implementation": "To cache results", + "symfony/doctrine-bridge": "To use Doctrine metadata", + "symfony/serializer": "To use Serializer metadata" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Property Info Component", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "time": "2018-07-26T08:55:25+00:00" + }, { "name": "symfony/routing", "version": "v4.1.3", @@ -5167,6 +5960,56 @@ ], "time": "2018-07-13T07:18:09+00:00" }, + { + "name": "webmozart/assert", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2018-01-29T19:49:41+00:00" + }, { "name": "white-october/pagerfanta-bundle", "version": "v1.2.1", @@ -5226,6 +6069,98 @@ ], "time": "2018-06-20T09:32:42+00:00" }, + { + "name": "willdurand/jsonp-callback-validator", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/JsonpCallbackValidator.git", + "reference": "1a7d388bb521959e612ef50c5c7b1691b097e909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/JsonpCallbackValidator/zipball/1a7d388bb521959e612ef50c5c7b1691b097e909", + "reference": "1a7d388bb521959e612ef50c5c7b1691b097e909", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~3.7" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonpCallbackValidator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "william.durand1@gmail.com", + "homepage": "http://www.willdurand.fr" + } + ], + "description": "JSONP callback validator.", + "time": "2014-01-20T22:35:06+00:00" + }, + { + "name": "willdurand/negotiation", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Negotiation.git", + "reference": "03436ededa67c6e83b9b12defac15384cb399dc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/03436ededa67c6e83b9b12defac15384cb399dc9", + "reference": "03436ededa67c6e83b9b12defac15384cb399dc9", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Negotiation\\": "src/Negotiation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + } + ], + "description": "Content Negotiation tools for PHP provided as a standalone library.", + "homepage": "http://williamdurand.fr/Negotiation/", + "keywords": [ + "accept", + "content", + "format", + "header", + "negotiation" + ], + "time": "2017-05-14T17:21:12+00:00" + }, { "name": "zendframework/zend-code", "version": "3.3.0", @@ -5332,6 +6267,68 @@ "zf2" ], "time": "2018-04-25T15:33:34+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "2.0.13", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "8b42fdc3d8c5a5e0d1f8d344aa359822c9f085e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/8b42fdc3d8c5a5e0d1f8d344aa359822c9f085e0", + "reference": "8b42fdc3d8c5a5e0d1f8d344aa359822c9f085e0", + "shasum": "" + }, + "require": { + "doctrine/annotations": "*", + "php": ">=5.6", + "symfony/finder": ">=2.2" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8.35 <=5.6", + "squizlabs/php_codesniffer": ">=2.7", + "zendframework/zend-form": "<2.8" + }, + "bin": [ + "bin/swagger" + ], + "type": "library", + "autoload": { + "psr-4": { + "Swagger\\": "src" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com", + "homepage": "http://www.zircote.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "http://bfanger.nl" + } + ], + "description": "Swagger-PHP - Generate interactive documentation for your RESTful API using phpdoc annotations", + "homepage": "https://github.com/zircote/swagger-php/", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "time": "2017-12-01T09:22:05+00:00" } ], "packages-dev": [ @@ -6062,158 +7059,6 @@ ], "time": "2018-02-15T16:58:55+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2017-09-11T18:02:19+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", - "shasum": "" - }, - "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "doctrine/instantiator": "~1.0.5", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-07-14T14:27:02+00:00" - }, { "name": "phpspec/prophecy", "version": "1.8.0", @@ -7488,16 +8333,16 @@ }, { "name": "symfony/maker-bundle", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "bb0485a3b2eecc561eff735109316677e18fb1d1" + "reference": "a1b0d9ff35219ec36c44fcc6213056cc339a1cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/bb0485a3b2eecc561eff735109316677e18fb1d1", - "reference": "bb0485a3b2eecc561eff735109316677e18fb1d1", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/a1b0d9ff35219ec36c44fcc6213056cc339a1cf8", + "reference": "a1b0d9ff35219ec36c44fcc6213056cc339a1cf8", "shasum": "" }, "require": { @@ -7549,7 +8394,7 @@ "scaffold", "scaffolding" ], - "time": "2018-05-17T19:26:29+00:00" + "time": "2018-08-07T16:05:09+00:00" }, { "name": "symfony/phpunit-bridge", @@ -7807,56 +8652,6 @@ ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "time": "2017-04-07T12:08:54+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2018-01-29T19:49:41+00:00" } ], "aliases": [], diff --git a/config/bundles.php b/config/bundles.php index 6ae64c70..9a367525 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -19,4 +19,7 @@ return [ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true], FOS\UserBundle\FOSUserBundle::class => ['all' => true], + FOS\RestBundle\FOSRestBundle::class => ['all' => true], + Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], + JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], ]; diff --git a/config/packages/dev/jms_serializer.yaml b/config/packages/dev/jms_serializer.yaml new file mode 100644 index 00000000..353e4602 --- /dev/null +++ b/config/packages/dev/jms_serializer.yaml @@ -0,0 +1,7 @@ +jms_serializer: + visitors: + json: + options: + - JSON_PRETTY_PRINT + - JSON_UNESCAPED_SLASHES + - JSON_PRESERVE_ZERO_FRACTION diff --git a/config/packages/fos_rest.yaml b/config/packages/fos_rest.yaml new file mode 100644 index 00000000..bbcac497 --- /dev/null +++ b/config/packages/fos_rest.yaml @@ -0,0 +1,107 @@ +fos_rest: +# disable_csrf_role: ROLE_API +# access_denied_listener: +# enabled: false +# service: null +# formats: +# name: ~ +# unauthorized_challenge: null +# param_fetcher_listener: +# enabled: false +# force: false +# service: null +# cache_dir: '%kernel.cache_dir%/fos_rest' +# allowed_methods_listener: +# enabled: false +# service: null + routing_loader: + default_format: json + include_format: true +# prefix_methods: true +# body_converter: +# enabled: false +# validate: false +# validation_errors_argument: validationErrors +# service: +# router: router +# templating: templating +# serializer: null +# view_handler: fos_rest.view_handler.default +# inflector: fos_rest.inflector.doctrine +# validator: validator +# serializer: +# version: null +# groups: [] +# serialize_null: false + view: + default_engine: twig +# force_redirects: +# name: ~ +# mime_types: +# enabled: false +# service: null +# formats: +# name: ~ +# formats: +# name: ~ +# templating_formats: +# name: ~ +# view_response_listener: +# enabled: true +# force: true +# service: null + failed_validation: 400 +# empty_content: 204 +# serialize_null: false +# jsonp_handler: +# callback_param: callback +# mime_type: application/javascript+jsonp + exception: + enabled: true + exception_controller: 'fos_rest.exception.controller:showAction' +# service: null + codes: +# 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404 +# 'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT +# 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': 400 + messages: + 'Symfony\Component\Routing\Exception\ResourceNotFoundException': true + 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': true +# body_listener: +# enabled: true +# service: null +# default_format: null +# throw_exception_on_unsupported_content_type: false +# decoders: +# name: ~ +# array_normalizer: +# service: null +# forms: false + format_listener: + enabled: true +# service: null + rules: + - { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json, xml ] } +# host: null # URL host name +# methods: null # Method for URL +# stop: false +# attributes: [] +# versioning: +# enabled: false +# default_version: ~ +# resolvers: +# query: +# enabled: true +# parameter_name: version +# custom_header: +# enabled: true +# header_name: X-Accept-Version +# media_type: +# enabled: true +# regex: /(v|version)=(?P[0-9\.]+)/ +# guessing_order: +# - query +# - custom_header +# - media_type + zone: + - { path: ^/api/* } diff --git a/config/packages/jms_serializer.yaml b/config/packages/jms_serializer.yaml new file mode 100644 index 00000000..33abf699 --- /dev/null +++ b/config/packages/jms_serializer.yaml @@ -0,0 +1,12 @@ +jms_serializer: + visitors: + xml: + format_output: '%kernel.debug%' + metadata: + directories: + FOSUB: + namespace_prefix: "FOS\\UserBundle" + path: "%kernel.root_dir%/../config/serializer/FOS/UserBundle" + App: + namespace_prefix: "App" + path: "%kernel.root_dir%/../config/serializer/App" diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml new file mode 100644 index 00000000..45535377 --- /dev/null +++ b/config/packages/nelmio_api_doc.yaml @@ -0,0 +1,27 @@ +nelmio_api_doc: + models: + use_jms: true + areas: + path_patterns: + - ^/api(?!/doc) + documentation: + #host: '{{hostname}}' + schemes: [http, https] + info: + title: Kimai 2 - API Docs + description: REST API description for the Kimai 2 time-tracking software + version: 0.1 + securityDefinitions: + X-AUTH-USER: + type: apiKey + description: 'Value: {Username}' + name: X-AUTH-USER + in: header + X-AUTH-TOKEN: + type: apiKey + description: 'Value: {API Token}' + name: X-AUTH-TOKEN + in: header + security: + - X-AUTH-USER: [] + - X-AUTH-TOKEN: [] diff --git a/config/packages/prod/jms_serializer.yaml b/config/packages/prod/jms_serializer.yaml new file mode 100644 index 00000000..bc97faf1 --- /dev/null +++ b/config/packages/prod/jms_serializer.yaml @@ -0,0 +1,6 @@ +jms_serializer: + visitors: + json: + options: + - JSON_UNESCAPED_SLASHES + - JSON_PRESERVE_ZERO_FRACTION diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 962633a7..a936332a 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -2,7 +2,6 @@ security: encoders: App\Entity\User: bcrypt - # https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers providers: fos_userbundle: id: fos_user.user_provider.username_email @@ -13,39 +12,28 @@ security: security: false secured_area: - # this firewall applies to all URLs pattern: ^/ - - # make sure only allowed users have access to their time-tracking user_checker: App\Security\UserChecker + anonymous: true + stateless: false - # but the firewall does not require login on every page - # denying access is done in access_control or in your controllers - anonymous: ~ + guard: + authenticators: + - App\Security\TokenAuthenticator remember_me: secret: '%kernel.secret%' lifetime: 604800 path: / - # This allows the user to login by submitting a username and password - # Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html form_login: - # fos user bundle handles the form login provider: fos_userbundle - # The route name that the login form submits to check_path: fos_user_security_check - # The name of the route where the login form lives - # When the user tries to access a protected page, they are redirected here login_path: fos_user_security_login - # Secure the login form against CSRF - # Reference: http://symfony.com/doc/current/cookbook/security/csrf_in_login_form.html csrf_token_generator: security.csrf.token_manager logout: - # The route name the user can go to in order to logout path: fos_user_security_logout - # The name of the route to redirect to after logging out target: homepage role_hierarchy: @@ -60,4 +48,4 @@ security: - { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN } - + - { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED } diff --git a/config/routes.yaml b/config/routes.yaml index f8d37d81..250b0a6e 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -11,15 +11,3 @@ homepage: _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction route: timesheet permanent: true - -fos_user_security: - prefix: /{_locale} - resource: "@FOSUserBundle/Resources/config/routing/security.xml" - -fos_user_registration: - prefix: /{_locale}/register - resource: "@FOSUserBundle/Resources/config/routing/registration.xml" - -fos_user_resetting: - prefix: /{_locale}/resetting - resource: "@FOSUserBundle/Resources/config/routing/resetting.xml" diff --git a/config/routes/annotations.yaml b/config/routes/annotations.yaml index 9028c556..1a86f800 100644 --- a/config/routes/annotations.yaml +++ b/config/routes/annotations.yaml @@ -5,4 +5,9 @@ controllers: requirements: _locale: '%app_locales%' defaults: - _locale: '%locale%' \ No newline at end of file + _locale: '%locale%' + +app.api: + resource: '../src/API/' + type: rest + prefix: /api diff --git a/config/routes/fos_user.yaml b/config/routes/fos_user.yaml new file mode 100644 index 00000000..40829ab2 --- /dev/null +++ b/config/routes/fos_user.yaml @@ -0,0 +1,14 @@ +# Expose security related features like login and logout +fos_user_security: + prefix: /{_locale} + resource: "@FOSUserBundle/Resources/config/routing/security.xml" + +# Expose the user registration feature +fos_user_registration: + prefix: /{_locale}/register + resource: "@FOSUserBundle/Resources/config/routing/registration.xml" + +# Expose the users password-reset feature +fos_user_resetting: + prefix: /{_locale}/resetting + resource: "@FOSUserBundle/Resources/config/routing/resetting.xml" diff --git a/config/routes/nelmio_api_doc.yaml b/config/routes/nelmio_api_doc.yaml new file mode 100644 index 00000000..eeedf1d9 --- /dev/null +++ b/config/routes/nelmio_api_doc.yaml @@ -0,0 +1,11 @@ +# Expose the Kimai API documentation as HTML +app.swagger_ui: + path: /api/doc + methods: GET + defaults: { _controller: nelmio_api_doc.controller.swagger_ui } + +# Expose the Kimai API documentation as swagger compliant JSON +app.swagger: + path: /api/doc.json + methods: GET + defaults: { _controller: nelmio_api_doc.controller.swagger } diff --git a/config/serializer/App/Entity.Activity.yml b/config/serializer/App/Entity.Activity.yml new file mode 100644 index 00000000..59aeb70e --- /dev/null +++ b/config/serializer/App/Entity.Activity.yml @@ -0,0 +1,18 @@ +App\Entity\Activity: + exclusion_policy: All + custom_accessor_order: [id, name, comment, visible, project_id] + properties: + id: + include: true + name: + include: true + comment: + include: true + visible: + include: true + virtual_properties: + getProjectId: + name: project_id + serialized_name: project_id + exp: "object.getProject() === null ? null : object.getProject().getId()" + type: integer diff --git a/config/serializer/App/Entity.Customer.yml b/config/serializer/App/Entity.Customer.yml new file mode 100644 index 00000000..8e33de48 --- /dev/null +++ b/config/serializer/App/Entity.Customer.yml @@ -0,0 +1,35 @@ +App\Entity\Customer: + exclusion_policy: All + properties: + id: + include: true + name: + include: true + number: + include: true + comment: + include: true + visible: + include: true + company: + include: true + contact: + include: true + address: + include: true + country: + include: true + currency: + include: true + phone: + include: true + fax: + include: true + mobile: + include: true + mail: + include: true + homepage: + include: true + timezone: + include: true diff --git a/config/serializer/App/Entity.Project.yml b/config/serializer/App/Entity.Project.yml new file mode 100644 index 00000000..d7446203 --- /dev/null +++ b/config/serializer/App/Entity.Project.yml @@ -0,0 +1,22 @@ +App\Entity\Project: + exclusion_policy: All + custom_accessor_order: [id, name, comment, visible, budget, orderNumber, customer_id] + properties: + id: + include: true + name: + include: true + comment: + include: true + visible: + include: true + budget: + include: true + orderNumber: + include: true + virtual_properties: + getCustomerId: + name: customer_id + serialized_name: customer_id + exp: "object.getCustomer() === null ? null : object.getCustomer().getId()" + type: integer diff --git a/config/serializer/App/Entity.User.yml b/config/serializer/App/Entity.User.yml new file mode 100644 index 00000000..29ccd21d --- /dev/null +++ b/config/serializer/App/Entity.User.yml @@ -0,0 +1,11 @@ +App\Entity\User: + exclusion_policy: All + properties: + id: + include: true + alias: + include: true + title: + include: true + avatar: + include: true diff --git a/config/serializer/FOS/UserBundle/Model.User.yml b/config/serializer/FOS/UserBundle/Model.User.yml new file mode 100644 index 00000000..b10eeb2b --- /dev/null +++ b/config/serializer/FOS/UserBundle/Model.User.yml @@ -0,0 +1,23 @@ +FOS\UserBundle\Model\User: + exclusion_policy: NONE + properties: + email: + exclude: true + emailCanonical: + exclude: true + usernameCanonical: + exclude: true + password: + exclude: true + plainPassword: + exclude: true + registeredAt: + exclude: true + lastLogin: + exclude: true + confirmationToken: + exclude: true + passwordRequestedAt: + exclude: true + salt: + exclude: true diff --git a/config/services.yaml b/config/services.yaml index 303d1fc9..fde402b0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -104,3 +104,27 @@ services: App\Timesheet\Calculator\RateCalculator: arguments: ["%kimai.timesheet.rates%"] + + # ================================================================================ + # REPOSITORIES + # ================================================================================ + + App\Repository\UserRepository: + class: Doctrine\ORM\EntityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: ['App\Entity\User'] + + App\Repository\ActivityRepository: + class: Doctrine\ORM\EntityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: ['App\Entity\Activity'] + + App\Repository\ProjectRepository: + class: Doctrine\ORM\EntityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: ['App\Entity\Project'] + + App\Repository\CustomerRepository: + class: Doctrine\ORM\EntityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: ['App\Entity\Customer'] diff --git a/config/services_test.yaml b/config/services_test.yaml index 876c36bd..c450b83d 100644 --- a/config/services_test.yaml +++ b/config/services_test.yaml @@ -2,8 +2,4 @@ services: _defaults: public: true - # If you need to access services in a test, create an alias - # and then fetch that alias from the container. As a convention, - # aliases are prefixed with test. For example: - # - # test.App\Service\MyService: '@App\Service\MyService' + test.PasswordEncoder: "@security.encoder_factory" \ No newline at end of file diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php new file mode 100644 index 00000000..c9d3a2af --- /dev/null +++ b/src/API/ActivityController.php @@ -0,0 +1,89 @@ +viewHandler = $viewHandler; + $this->repository = $repository; + } + + /** + * @SWG\Response( + * response=200, + * description="Returns the collection of all existing activities", + * @SWG\Schema(ref=@API\Model(type=Activity::class)), + * ) + * + * @return Response + */ + public function cgetAction() + { + $data = $this->repository->findAll(); + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } + + /** + * @SWG\Response( + * response=200, + * description="Returns one activity entity", + * @SWG\Schema(ref=@API\Model(type=Activity::class)), + * ) + * + * @param int $id + * @return Response + */ + public function getAction($id) + { + $data = $this->repository->find($id); + if (null === $data) { + throw new NotFoundException(); + } + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } +} diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php new file mode 100644 index 00000000..0cac8b21 --- /dev/null +++ b/src/API/CustomerController.php @@ -0,0 +1,89 @@ +viewHandler = $viewHandler; + $this->repository = $repository; + } + + /** + * @SWG\Response( + * response=200, + * description="Returns the collection of all existing customer", + * @SWG\Schema(ref=@Model(type=Customer::class)), + * ) + * + * @return Response + */ + public function cgetAction() + { + $data = $this->repository->findAll(); + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } + + /** + * @SWG\Response( + * response=200, + * description="Returns one customer entity", + * @SWG\Schema(ref=@Model(type=Customer::class)), + * ) + * + * @param int $id + * @return Response + */ + public function getAction($id) + { + $data = $this->repository->find($id); + if (null === $data) { + throw new NotFoundException(); + } + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } +} diff --git a/src/API/HealthcheckController.php b/src/API/HealthcheckController.php new file mode 100644 index 00000000..3f333d65 --- /dev/null +++ b/src/API/HealthcheckController.php @@ -0,0 +1,49 @@ +viewHandler = $viewHandler; + } + + /** + * @SWG\Response( + * response=200, + * description="A simple route that returns a 'pong', which you can use for testing the API", + * ) + * + * @Rest\Get(path="/ping") + */ + public function pingAction() + { + $view = new View(['message' => 'pong'], 200); + + return $this->viewHandler->handle($view); + } +} diff --git a/src/API/NotFoundException.php b/src/API/NotFoundException.php new file mode 100644 index 00000000..ce8a84a2 --- /dev/null +++ b/src/API/NotFoundException.php @@ -0,0 +1,22 @@ +viewHandler = $viewHandler; + $this->repository = $repository; + } + + /** + * @SWG\Response( + * response=200, + * description="Returns the collection of all existing projects", + * @SWG\Schema(ref=@Model(type=Project::class)), + * ) + * + * @return Response + */ + public function cgetAction() + { + $data = $this->repository->findAll(); + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } + + /** + * @SWG\Response( + * response=200, + * description="Returns one project entity", + * @SWG\Schema(ref=@Model(type=Project::class)), + * ) + * + * @param int $id + * @return Response + */ + public function getAction($id) + { + $data = $this->repository->find($id); + if (null === $data) { + throw new NotFoundException(); + } + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } +} diff --git a/src/API/UserController.php b/src/API/UserController.php new file mode 100644 index 00000000..7c787957 --- /dev/null +++ b/src/API/UserController.php @@ -0,0 +1,90 @@ +viewHandler = $viewHandler; + $this->repository = $repository; + } + + /** + * @SWG\Response( + * response=200, + * description="Returns the collection of all registered users", + * @SWG\Schema(ref=@Model(type=User::class)), + * ) + * + * @return Response + */ + public function cgetAction() + { + $data = $this->repository->findAll(); + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } + + /** + * @SWG\Response( + * response=200, + * description="Return one user entity", + * @SWG\Schema(ref=@Model(type=User::class)), + * ) + * + * @param int $id + * @return Response + */ + public function getAction($id) + { + $data = $this->repository->find($id); + if (null === $data) { + throw new NotFoundException(); + } + $view = new View($data, 200); + + return $this->viewHandler->handle($view); + } +} diff --git a/src/Calendar/TimesheetEntity.php b/src/Calendar/TimesheetEntity.php new file mode 100644 index 00000000..ccb4c39a --- /dev/null +++ b/src/Calendar/TimesheetEntity.php @@ -0,0 +1,268 @@ +id = $entry->getId(); + $this->start = $entry->getBegin(); + $this->title = $entry->getActivity()->getName(); + $this->description = $entry->getDescription(); + $this->customer = $entry->getActivity()->getProject()->getCustomer()->getName(); + $this->project = $entry->getActivity()->getProject()->getName(); + $this->activity = $entry->getActivity()->getName(); + + if (null === $entry->getEnd()) { + // TODO move these colors to the controller + $this->borderColor = '#f39c12'; + $this->backgroundColor = '#f39c12'; + } else { + $this->end = $entry->getEnd(); + } + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @param int $id + * @return TimesheetEntity + */ + public function setId(int $id) + { + $this->id = $id; + + return $this; + } + + /** + * @return \DateTime + */ + public function getStart(): \DateTime + { + return $this->start; + } + + /** + * @param \DateTime $start + * @return TimesheetEntity + */ + public function setStart(\DateTime $start) + { + $this->start = $start; + + return $this; + } + + /** + * @return \DateTime|null + */ + public function getEnd(): ?\DateTime + { + return $this->end; + } + + /** + * @param \DateTime|null $end + * @return TimesheetEntity + */ + public function setEnd(?\DateTime $end) + { + $this->end = $end; + + return $this; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + * @return TimesheetEntity + */ + public function setTitle(string $title) + { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @param string $description + * @return TimesheetEntity + */ + public function setDescription(string $description) + { + $this->description = $description; + + return $this; + } + + /** + * @return string + */ + public function getCustomer(): string + { + return $this->customer; + } + + /** + * @param string $customer + * @return TimesheetEntity + */ + public function setCustomer(string $customer) + { + $this->customer = $customer; + + return $this; + } + + /** + * @return string + */ + public function getProject(): string + { + return $this->project; + } + + /** + * @param string $project + * @return TimesheetEntity + */ + public function setProject(string $project) + { + $this->project = $project; + + return $this; + } + + /** + * @return string + */ + public function getActivity(): string + { + return $this->activity; + } + + /** + * @param string $activity + * @return TimesheetEntity + */ + public function setActivity(string $activity) + { + $this->activity = $activity; + + return $this; + } + + /** + * @return null|string + */ + public function getBorderColor(): ?string + { + return $this->borderColor; + } + + /** + * @param null|string $borderColor + * @return TimesheetEntity + */ + public function setBorderColor(?string $borderColor) + { + $this->borderColor = $borderColor; + + return $this; + } + + /** + * @return null|string + */ + public function getBackgroundColor(): ?string + { + return $this->backgroundColor; + } + + /** + * @param null|string $backgroundColor + * @return TimesheetEntity + */ + public function setBackgroundColor(?string $backgroundColor) + { + $this->backgroundColor = $backgroundColor; + + return $this; + } +} diff --git a/src/Controller/CalendarController.php b/src/Controller/CalendarController.php index 8e924187..3b83d381 100644 --- a/src/Controller/CalendarController.php +++ b/src/Controller/CalendarController.php @@ -10,6 +10,7 @@ namespace App\Controller; use App\Calendar\Service; +use App\Calendar\TimesheetEntity; use App\Entity\Timesheet; use App\Repository\Query\TimesheetQuery; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; @@ -96,35 +97,9 @@ class CalendarController extends AbstractController $result = []; foreach ($entries as $entry) { - $result[] = $this->getTimesheetEntryForCalendar($entry); + $result[] = new TimesheetEntity($entry); } return $this->json($result); } - - /** - * @param Timesheet $entry - * @return array - */ - protected function getTimesheetEntryForCalendar(Timesheet $entry) - { - $result = [ - 'id' => $entry->getId(), - 'start' => $entry->getBegin(), - 'title' => $entry->getActivity()->getName(), - 'description' => $entry->getDescription(), - 'customer' => $entry->getActivity()->getProject()->getCustomer()->getName(), - 'project' => $entry->getActivity()->getProject()->getName(), - 'activity' => $entry->getActivity()->getName(), - ]; - - if (null === $entry->getEnd()) { - $result['borderColor'] = '#f39c12'; - $result['backgroundColor'] = '#f39c12'; - } else { - $result['end'] = $entry->getEnd() ?? new \DateTime(); - } - - return $result; - } } diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index 4a9b1881..79b1472a 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -11,6 +11,7 @@ namespace App\Controller; use App\Entity\Timesheet; use App\Entity\User; +use App\Form\UserApiTokenType; use App\Form\UserEditType; use App\Form\UserPasswordType; use App\Form\UserPreferencesForm; @@ -104,6 +105,32 @@ class ProfileController extends AbstractController return $this->getProfileView($profile, 'password', null, $form); } + /** + * @Route("/{username}/api-token", name="user_profile_api_token") + * @Method({"GET", "POST"}) + * @Security("is_granted('api-token', profile)") + */ + public function apiTokenAction(User $profile, Request $request) + { + $form = $this->createApiTokenForm($profile); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $password = $this->encoder->encodePassword($profile, $profile->getPlainApiToken()); + $profile->setApiToken($password); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($profile); + $entityManager->flush(); + + $this->flashSuccess('action.updated_successfully'); + + return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]); + } + + return $this->getProfileView($profile, 'api-token', null, null, null, null, $form); + } + /** * @Route("/{username}/roles", name="user_profile_roles") * @Method({"GET", "POST"}) @@ -179,6 +206,7 @@ class ProfileController extends AbstractController * @param Form|null $pwdForm * @param Form|null $rolesForm * @param Form|null $prefsForm + * @param Form|null $apiTokenForm * @return \Symfony\Component\HttpFoundation\Response * @throws \Doctrine\ORM\NonUniqueResultException */ @@ -188,7 +216,8 @@ class ProfileController extends AbstractController Form $editForm = null, Form $pwdForm = null, Form $rolesForm = null, - Form $prefsForm = null + Form $prefsForm = null, + Form $apiTokenForm = null ) { /* @var $timesheetRepo TimesheetRepository */ $timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class); @@ -211,6 +240,10 @@ class ProfileController extends AbstractController $pwdForm = $pwdForm ?: $this->createPasswordForm($user); $viewVars['forms']['password'] = $pwdForm->createView(); } + if ($this->isGranted(UserVoter::API_TOKEN, $user)) { + $apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user); + $viewVars['forms']['api-token'] = $apiTokenForm->createView(); + } if ($this->isGranted(UserVoter::ROLES, $user)) { $rolesForm = $rolesForm ?: $this->createRolesForm($user); $viewVars['forms']['roles'] = $rolesForm->createView(); @@ -287,4 +320,21 @@ class ProfileController extends AbstractController ] ); } + + /** + * @param User $user + * @return \Symfony\Component\Form\FormInterface + */ + private function createApiTokenForm(User $user) + { + return $this->createForm( + UserApiTokenType::class, + $user, + [ + 'validation_groups' => ['apiTokenUpdate'], + 'action' => $this->generateUrl('user_profile_api_token', ['username' => $user->getUsername()]), + 'method' => 'POST' + ] + ); + } } diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index 127fee18..2ba74e4a 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -26,6 +26,7 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; class UserFixtures extends Fixture { public const DEFAULT_PASSWORD = 'kitten'; + public const DEFAULT_API_TOKEN = 'api_kitten'; public const DEFAULT_AVATAR = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y'; public const USERNAME_USER = 'john_user'; @@ -84,6 +85,7 @@ class UserFixtures extends Fixture ->setAvatar($userData[5]) ->setEnabled($userData[6]) ->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD)) + ->setApiToken($passwordEncoder->encodePassword($user, self::DEFAULT_API_TOKEN)) ->setPreferences([$this->getUserPreference($user)]) ; @@ -109,7 +111,7 @@ class UserFixtures extends Fixture } /** - * Generate randomized test users + * Generate randomized test users, which don't have API access. * * @param ObjectManager $manager */ diff --git a/src/Doctrine/TablePrefixSubscriber.php b/src/Doctrine/TablePrefixSubscriber.php index 46e854fd..8249ec36 100644 --- a/src/Doctrine/TablePrefixSubscriber.php +++ b/src/Doctrine/TablePrefixSubscriber.php @@ -11,6 +11,7 @@ namespace App\Doctrine; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\ORM\Events; /** * Adds a prefix to every doctrine entity AKA database table @@ -19,16 +20,27 @@ class TablePrefixSubscriber implements EventSubscriber { protected $prefix = ''; + /** + * @param string $prefix + */ public function __construct($prefix) { $this->prefix = (string) $prefix; } + /** + * @return array|string[] + */ public function getSubscribedEvents() { - return ['loadClassMetadata']; + return [ + Events::loadClassMetadata, + ]; } + /** + * @param LoadClassMetadataEventArgs $args + */ public function loadClassMetadata(LoadClassMetadataEventArgs $args) { $classMetadata = $args->getClassMetadata(); diff --git a/src/Entity/User.php b/src/Entity/User.php index 23b30ba7..df60bcb2 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -78,6 +78,18 @@ class User extends BaseUser implements UserInterface */ private $avatar; + /** + * @var string + * + * @ORM\Column(name="api_token", type="string", length=255, nullable=true) + */ + protected $apiToken; + + /** + * @var string + */ + protected $plainApiToken; + /** * @var UserPreference[]|Collection * @@ -179,6 +191,44 @@ class User extends BaseUser implements UserInterface return $this; } + /** + * @return string + */ + public function getApiToken() + { + return $this->apiToken; + } + + /** + * @param string $apiToken + * @return User + */ + public function setApiToken($apiToken) + { + $this->apiToken = $apiToken; + + return $this; + } + + /** + * @return string + */ + public function getPlainApiToken(): ?string + { + return $this->plainApiToken; + } + + /** + * @param string $plainApiToken + * @return User + */ + public function setPlainApiToken(string $plainApiToken) + { + $this->plainApiToken = $plainApiToken; + + return $this; + } + /** * @return UserPreference[]|Collection */ diff --git a/src/Form/UserApiTokenType.php b/src/Form/UserApiTokenType.php new file mode 100644 index 00000000..9d360d86 --- /dev/null +++ b/src/Form/UserApiTokenType.php @@ -0,0 +1,50 @@ +add('plainApiToken', RepeatedType::class, [ + 'type' => PasswordType::class, + 'first_options' => ['label' => 'label.api_token'], + 'second_options' => ['label' => 'label.api_token_repeat'], + ]) + ; + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + 'csrf_token_id' => 'edit_user_api_token', + ]); + } +} diff --git a/src/Form/UserPasswordType.php b/src/Form/UserPasswordType.php index a44412b5..54167f42 100644 --- a/src/Form/UserPasswordType.php +++ b/src/Form/UserPasswordType.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Defines the form used to create and manipulate Users. + * Defines the form used to set the users password. */ class UserPasswordType extends AbstractType { diff --git a/src/Migrations/Version20180701120000.php b/src/Migrations/Version20180701120000.php index ae0c46b0..238c0e68 100644 --- a/src/Migrations/Version20180701120000.php +++ b/src/Migrations/Version20180701120000.php @@ -16,7 +16,7 @@ use Doctrine\DBAL\Schema\Schema; /** * Initial database structure of Kimai 2. - * This file is only required for testing the migrations! + * This file is mainly required for testing the migrations. */ final class Version20180701120000 extends AbstractMigration { diff --git a/src/Migrations/Version20180730044139.php b/src/Migrations/Version20180730044139.php index bea885c6..6557e4f4 100644 --- a/src/Migrations/Version20180730044139.php +++ b/src/Migrations/Version20180730044139.php @@ -16,7 +16,9 @@ use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\Schema; /** - * Add constraints for the "delete user" feature. + * Migrations fot the "delete user" feature. + * + * Adds constraints to the timesheet table, so all timesheet entries. will be deleted when a user is deleted. */ final class Version20180730044139 extends AbstractMigration { diff --git a/src/Migrations/Version20180805183527.php b/src/Migrations/Version20180805183527.php new file mode 100644 index 00000000..258a5d15 --- /dev/null +++ b/src/Migrations/Version20180805183527.php @@ -0,0 +1,79 @@ +getPlatform(); + + if (!in_array($platform, ['sqlite', 'mysql'])) { + $this->abortIf(true, 'Unsupported database platform: ' . $platform); + } + + $user = $this->getTableName('users'); + + if ($platform === 'sqlite') { + $this->addSql('ALTER TABLE ' . $user . ' ADD COLUMN api_token VARCHAR(255) DEFAULT NULL'); + } else { + $this->addSql('ALTER TABLE ' . $user . ' ADD api_token VARCHAR(255) DEFAULT NULL'); + } + } + + /** + * @param Schema $schema + * @throws \Doctrine\DBAL\DBALException + * @throws \Doctrine\DBAL\Migrations\AbortMigrationException + */ + public function down(Schema $schema): void + { + $platform = $this->getPlatform(); + + if (!in_array($platform, ['sqlite', 'mysql'])) { + $this->abortIf(true, 'Unsupported database platform: ' . $platform); + } + + $user = $this->getTableName('user'); + + if ($platform === 'sqlite') { + $this->addSql('DROP INDEX UNIQ_B9AC5BCE92FC23A8'); + $this->addSql('DROP INDEX UNIQ_B9AC5BCEA0D96FBF'); + $this->addSql('DROP INDEX UNIQ_B9AC5BCEC05FB297'); + $this->addSql('DROP INDEX UNIQ_B9AC5BCEF85E0677'); + $this->addSql('DROP INDEX UNIQ_B9AC5BCEE7927C74'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $user . ' AS SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar FROM ' . $user); + $this->addSql('DROP TABLE ' . $user); + $this->addSql('CREATE TABLE ' . $user . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, username_canonical VARCHAR(180) NOT NULL, email VARCHAR(180) NOT NULL, email_canonical VARCHAR(180) NOT NULL, enabled BOOLEAN NOT NULL, salt VARCHAR(255) DEFAULT NULL, password VARCHAR(255) NOT NULL, last_login DATETIME DEFAULT NULL, confirmation_token VARCHAR(180) DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, roles CLOB NOT NULL, alias VARCHAR(60) DEFAULT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL)'); + $this->addSql('INSERT INTO ' . $user . ' (id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar) SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar FROM __temp__' . $user); + $this->addSql('DROP TABLE __temp__' . $user); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON ' . $user . ' (username_canonical)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON ' . $user . ' (email_canonical)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEC05FB297 ON ' . $user . ' (confirmation_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEF85E0677 ON ' . $user . ' (username)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEE7927C74 ON ' . $user . ' (email)'); + } else { + $this->addSql('ALTER TABLE ' . $user . ' DROP api_token'); + } + } +} diff --git a/src/Security/TokenAuthenticator.php b/src/Security/TokenAuthenticator.php new file mode 100644 index 00000000..a4dadc9c --- /dev/null +++ b/src/Security/TokenAuthenticator.php @@ -0,0 +1,164 @@ +encoderFactory = $encoderFactory; + } + + /** + * @param Request $request + * @return bool + */ + public function supports(Request $request) + { + if (strpos($request->getRequestUri(), '/api/doc') === 0) { + return false; + } + + if (strpos($request->getRequestUri(), '/api/') === 0) { + // javascript requests can set a header to disable this authenticator and use the existing session + return !$request->headers->has(self::HEADER_JAVASCRIPT); + } + + return false; + } + + /** + * @param Request $request + * @return array|bool + */ + public function getCredentials(Request $request) + { + return [ + 'user' => $request->headers->get(self::HEADER_USERNAME), + 'token' => $request->headers->get(self::HEADER_TOKEN), + ]; + } + + /** + * @param array $credentials + * @param UserProviderInterface $userProvider + * @return null|UserInterface + */ + public function getUser($credentials, UserProviderInterface $userProvider) + { + $token = $credentials['token'] ?? null; + $user = $credentials['user'] ?? null; + + if (empty($token) || empty($user)) { + return null; + } + + return $userProvider->loadUserByUsername($user); + } + + /** + * @param array $credentials + * @param UserInterface $user + * @return bool + */ + public function checkCredentials($credentials, UserInterface $user) + { + $token = $credentials['token']; + + if (!empty($token) && $user instanceof User && !empty($user->getApiToken())) { + $encoder = $this->encoderFactory->getEncoder($user); + + return $encoder->isPasswordValid($user->getApiToken(), $token, $user->getSalt()); + } + + return false; + } + + /** + * @param Request $request + * @param TokenInterface $token + * @param string $providerKey + * @return null|Response + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + return null; + } + + /** + * @param Request $request + * @param AuthenticationException $exception + * @return null|JsonResponse|Response + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + if (!$request->headers->has(self::HEADER_USERNAME) || !$request->headers->has(self::HEADER_TOKEN)) { + return new JsonResponse( + ['message' => 'Authentication required, missing headers: ' . self::HEADER_USERNAME . ', ' . self::HEADER_TOKEN], + Response::HTTP_FORBIDDEN + ); + } + + $data = [ + 'message' => 'Invalid credentials' + + // security measure: do not leak real reason (unknown user, invalid credentials ...) + // you can uncomment this for debugging + // 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) + ]; + + return new JsonResponse($data, Response::HTTP_FORBIDDEN); + } + + /** + * @param Request $request + * @param AuthenticationException|null $authException + * @return JsonResponse|Response + */ + public function start(Request $request, AuthenticationException $authException = null) + { + $data = [ + 'message' => 'Authentication required, missing headers: ' . self::HEADER_USERNAME . ', ' . self::HEADER_TOKEN + ]; + + return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); + } + + /** + * @return bool + */ + public function supportsRememberMe() + { + return false; + } +} diff --git a/src/Voter/UserVoter.php b/src/Voter/UserVoter.php index 0d595cfe..bda84e76 100644 --- a/src/Voter/UserVoter.php +++ b/src/Voter/UserVoter.php @@ -24,6 +24,7 @@ class UserVoter extends AbstractVoter public const PASSWORD = 'password'; public const ROLES = 'roles'; public const PREFERENCES = 'preferences'; + public const API_TOKEN = 'api-token'; public const ALLOWED_ATTRIBUTES = [ self::VIEW, @@ -32,7 +33,8 @@ class UserVoter extends AbstractVoter self::ROLES, self::PASSWORD, self::DELETE, - self::PREFERENCES + self::PREFERENCES, + self::API_TOKEN, ]; /** @@ -71,6 +73,7 @@ class UserVoter extends AbstractVoter case self::VIEW: return $this->canView($subject, $user, $token); case self::EDIT: + case self::API_TOKEN: case self::PASSWORD: return $this->canEdit($subject, $user, $token); case self::DELETE: diff --git a/symfony.lock b/symfony.lock index 1f255014..6b7f47b1 100644 --- a/symfony.lock +++ b/symfony.lock @@ -110,6 +110,9 @@ "erusev/parsedown": { "version": "1.6.4" }, + "exsyst/swagger": { + "version": "v0.4.1" + }, "friendsofphp/php-cs-fixer": { "version": "2.2", "recipe": { @@ -119,6 +122,15 @@ "ref": "bb31a3bbec00a8fc8aa1c9fbf9b0ef9fc492f93d" } }, + "friendsofsymfony/rest-bundle": { + "version": "2.2", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "2.2", + "ref": "258300d52be6ad59b32a888d5ddafbf9638540ff" + } + }, "friendsofsymfony/user-bundle": { "version": "v2.1.2" }, @@ -128,6 +140,24 @@ "jdorn/sql-formatter": { "version": "v1.2.17" }, + "jms/metadata": { + "version": "1.6.0" + }, + "jms/parser-lib": { + "version": "1.0.0" + }, + "jms/serializer": { + "version": "1.13.0" + }, + "jms/serializer-bundle": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "2.0", + "ref": "fe60ce509ef04a3f40da96e3979bc8d9b13b2372" + } + }, "kevinpapst/adminlte-bundle": { "version": "0.3" }, @@ -137,6 +167,15 @@ "myclabs/deep-copy": { "version": "1.7.0" }, + "nelmio/api-doc-bundle": { + "version": "3.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "3.0", + "ref": "c8e0c38e1a280ab9e37587a8fa32b251d5bc1c94" + } + }, "nikic/php-parser": { "version": "v4.0.2" }, @@ -161,6 +200,9 @@ "php-cs-fixer/diff": { "version": "v1.2.0" }, + "phpcollection/phpcollection": { + "version": "0.5.0" + }, "phpdocumentor/reflection-common": { "version": "1.0.1" }, @@ -170,6 +212,9 @@ "phpdocumentor/type-resolver": { "version": "0.4.0" }, + "phpoption/phpoption": { + "version": "1.5.0" + }, "phpspec/prophecy": { "version": "1.7.3" }, @@ -413,6 +458,9 @@ "symfony/property-access": { "version": "v4.0.3" }, + "symfony/property-info": { + "version": "v4.1.3" + }, "symfony/routing": { "version": "4.0", "recipe": { @@ -536,10 +584,19 @@ "white-october/pagerfanta-bundle": { "version": "v1.1.2" }, + "willdurand/jsonp-callback-validator": { + "version": "v1.1.0" + }, + "willdurand/negotiation": { + "version": "v2.3.1" + }, "zendframework/zend-code": { "version": "3.3.0" }, "zendframework/zend-eventmanager": { "version": "3.2.0" + }, + "zircote/swagger-php": { + "version": "2.0.13" } } diff --git a/templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig b/templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig new file mode 100644 index 00000000..37aeb53e --- /dev/null +++ b/templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig @@ -0,0 +1,18 @@ + + + + + {{ swagger_data.spec.info.title }} + + + + {# json_encode(65) is for JSON_UNESCAPED_SLASHES|JSON_HEX_TAG to avoid JS XSS #} + + + +
+ + + + + diff --git a/tests/API/APIControllerBaseTest.php b/tests/API/APIControllerBaseTest.php new file mode 100644 index 00000000..b38c8d9c --- /dev/null +++ b/tests/API/APIControllerBaseTest.php @@ -0,0 +1,169 @@ + UserFixtures::USERNAME_SUPER_ADMIN, + 'HTTP_X_AUTH_TOKEN' => UserFixtures::DEFAULT_API_TOKEN, + ]); + break; + + case User::ROLE_ADMIN: + $client = self::createClient([], [ + 'HTTP_X_AUTH_USER' => UserFixtures::USERNAME_ADMIN, + 'HTTP_X_AUTH_TOKEN' => UserFixtures::DEFAULT_API_TOKEN, + ]); + break; + + case User::ROLE_TEAMLEAD: + $client = self::createClient([], [ + 'HTTP_X_AUTH_USER' => UserFixtures::USERNAME_TEAMLEAD, + 'HTTP_X_AUTH_TOKEN' => UserFixtures::DEFAULT_API_TOKEN, + ]); + break; + + case User::ROLE_USER: + $client = self::createClient([], [ + 'HTTP_X_AUTH_USER' => UserFixtures::USERNAME_USER, + 'HTTP_X_AUTH_TOKEN' => UserFixtures::DEFAULT_API_TOKEN, + ]); + break; + + default: + $client = null; + break; + } + + return $client; + } + + /** + * @param string $url + * @param bool $json + * @return string + */ + protected function createUrl($url, $json = true) + { + return '/' . ltrim($url, '/') . ($json ? '.json' : ''); + } + + /** + * @param Client $client + * @param string $url + * @param string $method + */ + protected function assertRequestIsSecured(Client $client, string $url, $method = 'GET') + { + $this->request($client, $url, $method); + $this->assertResponseIsSecured($client->getResponse(), $url); + } + + /** + * @param Response $response + * @param string $url + */ + protected function assertResponseIsSecured(Response $response, string $url) + { + $data = ['message' => 'Authentication required, missing headers: X-AUTH-USER, X-AUTH-TOKEN']; + + $this->assertEquals( + $data, + json_decode($response->getContent(), true), + sprintf('The secure URL %s is not protected.', $url) + ); + + $this->assertEquals( + Response::HTTP_FORBIDDEN, // TODO that should actually be Response::HTTP_UNAUTHORIZED + $response->getStatusCode(), + sprintf('The secure URL %s has the wrong status code %s.', $url, $response->getStatusCode()) + ); + } + + /** + * @param string $role + * @param string $url + * @param string $method + */ + protected function assertUrlIsSecuredForRole(string $role, string $url, string $method = 'GET') + { + $client = $this->getClientForAuthenticatedUser($role); + $client->request($method, $this->createUrl($url)); + + $this->assertFalse( + $client->getResponse()->isSuccessful(), + sprintf('The secure URL %s is not protected for role %s', $url, $role) + ); + + $expected = [ + 'code' => 403, + 'message' => 'Access denied.' + ]; + + $this->assertEquals(403, $client->getResponse()->getStatusCode()); + + $this->assertEquals( + $expected, + json_decode($client->getResponse()->getContent(), true) + ); + } + + /** + * @param Client $client + * @param string $url + * @param string $method + * @return Crawler + */ + protected function request(Client $client, string $url, $method = 'GET') + { + return $client->request($method, $this->createUrl($url), [], [], ['HTTP_CONTENT_TYPE' => 'application/json']); + } + + /** + * @param string $role + * @param string $url + */ + protected function assertEntityNotFound(string $role, string $url) + { + $client = $this->getClientForAuthenticatedUser($role); + $this->request($client, $url); + + $expected = [ + 'code' => 404, + 'message' => 'Not found' + ]; + + $this->assertEquals(404, $client->getResponse()->getStatusCode()); + + $this->assertEquals( + $expected, + json_decode($client->getResponse()->getContent(), true) + ); + } +} diff --git a/tests/API/ActivityControllerTest.php b/tests/API/ActivityControllerTest.php new file mode 100644 index 00000000..54613a85 --- /dev/null +++ b/tests/API/ActivityControllerTest.php @@ -0,0 +1,64 @@ +assertUrlIsSecured('/api/activities'); + } + + public function testGetCollection() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/activities'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertNotEmpty($result); + $this->assertEquals(1, count($result)); + $this->assertStructure($result[0]); + } + + public function testGetEntity() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/activities/1'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertStructure($result); + } + + public function testNotFound() + { + $this->assertEntityNotFound(User::ROLE_USER, '/api/activities/2'); + } + + protected function assertStructure(array $result) + { + $expectedKeys = [ + 'id', 'name', 'comment', 'visible', 'project_id' + ]; + + $actual = array_keys($result); + + $this->assertEquals(count($expectedKeys), count($actual), 'Activity entity has different amount of keys'); + $this->assertEquals($expectedKeys, $actual, 'Activity structure does not match'); + } +} diff --git a/tests/API/ApiDocControllerTest.php b/tests/API/ApiDocControllerTest.php new file mode 100644 index 00000000..dff04e9d --- /dev/null +++ b/tests/API/ApiDocControllerTest.php @@ -0,0 +1,51 @@ +assertUrlIsSecured('/api/doc'); + } + + public function testGetDocs() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/doc'); + $this->assertContains('Kimai 2 - API Docs', $client->getResponse()->getContent()); + } + + public function testGetJsonDocs() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/doc.json'); + $this->assertContains('"title":"Kimai 2 - API Docs"', $client->getResponse()->getContent()); + $result = json_decode($client->getResponse()->getContent(), true); + $this->assertInternalType('array', $result); + $this->assertNotEmpty($result); + } + + /** + * @param string $url + * @return string + */ + protected function createUrl($url) + { + return '/' . ltrim($url, '/'); + } +} diff --git a/tests/API/CustomerControllerTest.php b/tests/API/CustomerControllerTest.php new file mode 100644 index 00000000..df7e7cd9 --- /dev/null +++ b/tests/API/CustomerControllerTest.php @@ -0,0 +1,65 @@ +assertUrlIsSecured('/api/customers'); + } + + public function testGetCollection() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/customers'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertNotEmpty($result); + $this->assertEquals(1, count($result)); + $this->assertStructure($result[0]); + } + + public function testGetEntity() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/customers/1'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertStructure($result); + } + + public function testNotFound() + { + $this->assertEntityNotFound(User::ROLE_USER, '/api/customers/2'); + } + + protected function assertStructure(array $result) + { + $expectedKeys = [ + 'id', 'name', 'number', 'comment', 'visible', 'company', 'contact', 'address', 'country', 'currency', + 'phone', 'fax', 'mobile', 'mail', 'timezone' + ]; + + $actual = array_keys($result); + + $this->assertEquals(count($expectedKeys), count($actual), 'Customer entity has different amount of keys'); + $this->assertEquals($expectedKeys, $actual, 'Customer structure does not match'); + } +} diff --git a/tests/API/HealthcheckControllerTest.php b/tests/API/HealthcheckControllerTest.php new file mode 100644 index 00000000..10740bee --- /dev/null +++ b/tests/API/HealthcheckControllerTest.php @@ -0,0 +1,35 @@ +assertUrlIsSecured('/api/ping'); + } + + public function testPing() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/ping'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertEquals(['message' => 'pong'], $result); + } +} diff --git a/tests/API/ProjectControllerTest.php b/tests/API/ProjectControllerTest.php new file mode 100644 index 00000000..00d7b26b --- /dev/null +++ b/tests/API/ProjectControllerTest.php @@ -0,0 +1,64 @@ +assertUrlIsSecured('/api/projects'); + } + + public function testGetCollection() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/projects'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertNotEmpty($result); + $this->assertEquals(1, count($result)); + $this->assertStructure($result[0]); + } + + public function testGetEntity() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/projects/1'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertStructure($result); + } + + public function testNotFound() + { + $this->assertEntityNotFound(User::ROLE_USER, '/api/projects/2'); + } + + protected function assertStructure(array $result) + { + $expectedKeys = [ + 'id', 'name', 'comment', 'visible', 'budget', 'order_number', 'customer_id' + ]; + + $actual = array_keys($result); + + $this->assertEquals(count($expectedKeys), count($actual), 'Project entity has different amount of keys'); + $this->assertEquals($expectedKeys, $actual, 'Project structure does not match'); + } +} diff --git a/tests/API/UserControllerTest.php b/tests/API/UserControllerTest.php new file mode 100644 index 00000000..3e839024 --- /dev/null +++ b/tests/API/UserControllerTest.php @@ -0,0 +1,65 @@ +assertUrlIsSecured('/api/users'); + $this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/api/users'); + } + + public function testGetCollection() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); + $this->assertAccessIsGranted($client, '/api/users'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertNotEmpty($result); + $this->assertEquals(6, count($result)); + $this->assertStructure($result[0]); + } + + public function testGetEntity() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); + $this->assertAccessIsGranted($client, '/api/users/1'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertStructure($result); + } + + public function testNotFound() + { + $this->assertEntityNotFound(User::ROLE_SUPER_ADMIN, '/api/users/99'); + } + + protected function assertStructure(array $result) + { + $expectedKeys = [ + 'id', 'username', 'enabled', 'roles', 'alias', 'title', 'avatar' + ]; + + $actual = array_keys($result); + + $this->assertEquals(count($expectedKeys), count($actual), 'User entity has different amount of keys'); + $this->assertEquals($expectedKeys, $actual, 'User structure does not match'); + } +} diff --git a/tests/Controller/Admin/UserControllerTest.php b/tests/Controller/Admin/UserControllerTest.php index 08d52be0..b479ccbc 100644 --- a/tests/Controller/Admin/UserControllerTest.php +++ b/tests/Controller/Admin/UserControllerTest.php @@ -52,15 +52,13 @@ class UserControllerTest extends ControllerBaseTest $this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit')); $client->followRedirect(); + $expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#roles']; + $tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li'); - $this->assertEquals(4, $tabs->count()); - $expectedTabs = ['#charts', '#settings', '#password', '#roles']; + $this->assertEquals(count($expectedTabs), $tabs->count()); $foundTabs = []; foreach ($tabs->filter('a') as $tab) { - $name = $tab->getAttribute('href'); - if (in_array($name, $expectedTabs)) { - $foundTabs[] = $name; - } + $foundTabs[] = $tab->getAttribute('href'); } $this->assertEmpty(array_diff($expectedTabs, $foundTabs)); diff --git a/tests/Controller/CalendarControllerTest.php b/tests/Controller/CalendarControllerTest.php index 5ab9f181..e6d718e7 100644 --- a/tests/Controller/CalendarControllerTest.php +++ b/tests/Controller/CalendarControllerTest.php @@ -72,5 +72,27 @@ class CalendarControllerTest extends ControllerBaseTest $this->assertInternalType('array', $json); $this->assertNotEmpty($json); $this->assertEquals(10, count($json)); + foreach ($json as $result) { + $this->assertInternalType('array', $result); + $this->assertCalendarStructure($result); + } + } + + protected function assertCalendarStructure(array $result) + { + $this->assertArrayHasKey('id', $result); + $this->assertArrayHasKey('start', $result); + $this->assertArrayHasKey('title', $result); + $this->assertArrayHasKey('description', $result); + $this->assertArrayHasKey('customer', $result); + $this->assertArrayHasKey('project', $result); + $this->assertArrayHasKey('activity', $result); + $this->assertArrayHasKey('borderColor', $result); + $this->assertArrayHasKey('backgroundColor', $result); + + if (isset($result['end'])) { + $this->assertNull($result['borderColor']); + $this->assertNull($result['backgroundColor']); + } } } diff --git a/tests/Controller/ControllerBaseTest.php b/tests/Controller/ControllerBaseTest.php index 3fc80771..45bd5a64 100644 --- a/tests/Controller/ControllerBaseTest.php +++ b/tests/Controller/ControllerBaseTest.php @@ -96,18 +96,18 @@ abstract class ControllerBaseTest extends WebTestCase */ protected function assertRequestIsSecured(Client $client, string $url, $method = 'GET') { - $client->request($method, $this->createUrl($url)); + $this->request($client, $url, $method); /* @var RedirectResponse $response */ $response = $client->getResponse(); $this->assertTrue( $response->isRedirect(), - sprintf('The secure URL %s is not protected.', $url . $response->getContent()) + sprintf('The secure URL %s is not protected.', $url) ); - $this->assertEquals( - 'http://localhost' . $this->createUrl('/login'), + $this->assertStringEndsWith( + '/login', $response->getTargetUrl(), sprintf('The secure URL %s does not redirect to the login form.', $url) ); diff --git a/tests/Controller/ProfileControllerTest.php b/tests/Controller/ProfileControllerTest.php index b50f47db..c5f5fd3e 100644 --- a/tests/Controller/ProfileControllerTest.php +++ b/tests/Controller/ProfileControllerTest.php @@ -10,10 +10,13 @@ namespace App\Tests\Controller; use App\DataFixtures\UserFixtures; +use App\Entity\User; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; /** * @coversDefaultClass \App\Controller\InvoiceController * @group integration + * @group legacy */ class ProfileControllerTest extends ControllerBaseTest { @@ -28,15 +31,13 @@ class ProfileControllerTest extends ControllerBaseTest $this->request($client, '/profile/' . UserFixtures::USERNAME_USER); $this->assertTrue($client->getResponse()->isSuccessful()); + $expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#preferences']; + $tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li'); - $this->assertEquals(4, $tabs->count()); - $expectedTabs = ['#charts', '#settings', '#password', '#preferences']; + $this->assertEquals(count($expectedTabs), $tabs->count()); $foundTabs = []; foreach ($tabs->filter('a') as $tab) { - $name = $tab->getAttribute('href'); - if (in_array($name, $expectedTabs)) { - $foundTabs[] = $name; - } + $foundTabs[] = $tab->getAttribute('href'); } $this->assertEmpty(array_diff($expectedTabs, $foundTabs)); } @@ -47,4 +48,74 @@ class ProfileControllerTest extends ControllerBaseTest $this->request($client, '/profile/' . UserFixtures::USERNAME_TEAMLEAD); $this->assertFalse($client->getResponse()->isSuccessful()); } + + public function testUpdateApiToken() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->request($client, '/profile/' . UserFixtures::USERNAME_USER); + + /** @var User $user */ + $user = $client->getContainer()->get('security.token_storage')->getToken()->getUser(); + /** @var EncoderFactoryInterface $passwordEncoder */ + $passwordEncoder = $client->getContainer()->get('test.PasswordEncoder'); + + $this->assertTrue($passwordEncoder->getEncoder($user)->isPasswordValid($user->getApiToken(), UserFixtures::DEFAULT_API_TOKEN, $user->getSalt())); + $this->assertFalse($passwordEncoder->getEncoder($user)->isPasswordValid($user->getApiToken(), 'test123', $user->getSalt())); + $this->assertEquals(UserFixtures::USERNAME_USER, $user->getUsername()); + + $form = $client->getCrawler()->filter('form[name=user_api_token]')->form(); + $client->submit($form, [ + 'user_api_token' => [ + 'plainApiToken' => [ + 'first' => 'test123', + 'second' => 'test123', + ] + ] + ]); + + $this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode(UserFixtures::USERNAME_USER))); + $client->followRedirect(); + $this->assertTrue($client->getResponse()->isSuccessful()); + + $this->assertHasFlashSuccess($client); + + $user = $client->getContainer()->get('security.token_storage')->getToken()->getUser(); + $this->assertFalse($passwordEncoder->getEncoder($user)->isPasswordValid($user->getApiToken(), UserFixtures::DEFAULT_API_TOKEN, $user->getSalt())); + $this->assertTrue($passwordEncoder->getEncoder($user)->isPasswordValid($user->getApiToken(), 'test123', $user->getSalt())); + } + + public function testUpdatePassword() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->request($client, '/profile/' . UserFixtures::USERNAME_USER); + + /** @var User $user */ + $user = $client->getContainer()->get('security.token_storage')->getToken()->getUser(); + /** @var EncoderFactoryInterface $passwordEncoder */ + $passwordEncoder = $client->getContainer()->get('test.PasswordEncoder'); + + $this->assertTrue($passwordEncoder->getEncoder($user)->isPasswordValid($user->getPassword(), UserFixtures::DEFAULT_PASSWORD, $user->getSalt())); + $this->assertFalse($passwordEncoder->getEncoder($user)->isPasswordValid($user->getPassword(), 'test123', $user->getSalt())); + $this->assertEquals(UserFixtures::USERNAME_USER, $user->getUsername()); + + $form = $client->getCrawler()->filter('form[name=user_password]')->form(); + $client->submit($form, [ + 'user_password' => [ + 'plainPassword' => [ + 'first' => 'test123', + 'second' => 'test123', + ] + ] + ]); + + $this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode(UserFixtures::USERNAME_USER))); + $client->followRedirect(); + $this->assertTrue($client->getResponse()->isSuccessful()); + + $this->assertHasFlashSuccess($client); + + $user = $client->getContainer()->get('security.token_storage')->getToken()->getUser(); + $this->assertFalse($passwordEncoder->getEncoder($user)->isPasswordValid($user->getPassword(), UserFixtures::DEFAULT_PASSWORD, $user->getSalt())); + $this->assertTrue($passwordEncoder->getEncoder($user)->isPasswordValid($user->getPassword(), 'test123', $user->getSalt())); + } } diff --git a/translations/messages.de.xliff b/translations/messages.de.xliff index e2b57919..6c2c0fbd 100644 --- a/translations/messages.de.xliff +++ b/translations/messages.de.xliff @@ -63,6 +63,14 @@ label.user_profile Mein Profil + + label.api_token + API Passwort + + + label.api_token_repeat + API Passwort wiederholen +