Added basic API endpoints (#258)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
###> phpunit/phpunit ###
|
###> phpunit/phpunit ###
|
||||||
/phpunit.xml
|
/phpunit.xml
|
||||||
|
/.phpunit.result.cache
|
||||||
###< phpunit/phpunit ###
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
###> friendsofphp/php-cs-fixer ###
|
###> friendsofphp/php-cs-fixer ###
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
"ext-pdo_sqlite": "*",
|
"ext-pdo_sqlite": "*",
|
||||||
"beberlei/DoctrineExtensions": "^1.0",
|
"beberlei/DoctrineExtensions": "^1.0",
|
||||||
"erusev/parsedown": "^1.6",
|
"erusev/parsedown": "^1.6",
|
||||||
|
"friendsofsymfony/rest-bundle": "^2.3",
|
||||||
"friendsofsymfony/user-bundle": "~2.0",
|
"friendsofsymfony/user-bundle": "~2.0",
|
||||||
|
"jms/serializer-bundle": "^2.4",
|
||||||
"kevinpapst/adminlte-bundle": "^1.2",
|
"kevinpapst/adminlte-bundle": "^1.2",
|
||||||
|
"nelmio/api-doc-bundle": "^3.2",
|
||||||
"ocramius/proxy-manager": "2.1.1",
|
"ocramius/proxy-manager": "2.1.1",
|
||||||
"sensio/framework-extra-bundle": "^5.1",
|
"sensio/framework-extra-bundle": "^5.1",
|
||||||
"symfony/asset": "^4.0",
|
"symfony/asset": "^4.0",
|
||||||
@@ -92,7 +95,7 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"symfony": {
|
"symfony": {
|
||||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||||
"allow-contrib": false
|
"allow-contrib": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repositories": [
|
"repositories": [
|
||||||
|
|||||||
1211
composer.lock
generated
1211
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,4 +19,7 @@ return [
|
|||||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||||
KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true],
|
KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true],
|
||||||
FOS\UserBundle\FOSUserBundle::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],
|
||||||
];
|
];
|
||||||
|
|||||||
7
config/packages/dev/jms_serializer.yaml
Normal file
7
config/packages/dev/jms_serializer.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
jms_serializer:
|
||||||
|
visitors:
|
||||||
|
json:
|
||||||
|
options:
|
||||||
|
- JSON_PRETTY_PRINT
|
||||||
|
- JSON_UNESCAPED_SLASHES
|
||||||
|
- JSON_PRESERVE_ZERO_FRACTION
|
||||||
107
config/packages/fos_rest.yaml
Normal file
107
config/packages/fos_rest.yaml
Normal file
@@ -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<version>[0-9\.]+)/
|
||||||
|
# guessing_order:
|
||||||
|
# - query
|
||||||
|
# - custom_header
|
||||||
|
# - media_type
|
||||||
|
zone:
|
||||||
|
- { path: ^/api/* }
|
||||||
12
config/packages/jms_serializer.yaml
Normal file
12
config/packages/jms_serializer.yaml
Normal file
@@ -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"
|
||||||
27
config/packages/nelmio_api_doc.yaml
Normal file
27
config/packages/nelmio_api_doc.yaml
Normal file
@@ -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: []
|
||||||
6
config/packages/prod/jms_serializer.yaml
Normal file
6
config/packages/prod/jms_serializer.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
jms_serializer:
|
||||||
|
visitors:
|
||||||
|
json:
|
||||||
|
options:
|
||||||
|
- JSON_UNESCAPED_SLASHES
|
||||||
|
- JSON_PRESERVE_ZERO_FRACTION
|
||||||
@@ -2,7 +2,6 @@ security:
|
|||||||
encoders:
|
encoders:
|
||||||
App\Entity\User: bcrypt
|
App\Entity\User: bcrypt
|
||||||
|
|
||||||
# https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
|
|
||||||
providers:
|
providers:
|
||||||
fos_userbundle:
|
fos_userbundle:
|
||||||
id: fos_user.user_provider.username_email
|
id: fos_user.user_provider.username_email
|
||||||
@@ -13,39 +12,28 @@ security:
|
|||||||
security: false
|
security: false
|
||||||
|
|
||||||
secured_area:
|
secured_area:
|
||||||
# this firewall applies to all URLs
|
|
||||||
pattern: ^/
|
pattern: ^/
|
||||||
|
|
||||||
# make sure only allowed users have access to their time-tracking
|
|
||||||
user_checker: App\Security\UserChecker
|
user_checker: App\Security\UserChecker
|
||||||
|
anonymous: true
|
||||||
|
stateless: false
|
||||||
|
|
||||||
# but the firewall does not require login on every page
|
guard:
|
||||||
# denying access is done in access_control or in your controllers
|
authenticators:
|
||||||
anonymous: ~
|
- App\Security\TokenAuthenticator
|
||||||
|
|
||||||
remember_me:
|
remember_me:
|
||||||
secret: '%kernel.secret%'
|
secret: '%kernel.secret%'
|
||||||
lifetime: 604800
|
lifetime: 604800
|
||||||
path: /
|
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:
|
form_login:
|
||||||
# fos user bundle handles the form login
|
|
||||||
provider: fos_userbundle
|
provider: fos_userbundle
|
||||||
# The route name that the login form submits to
|
|
||||||
check_path: fos_user_security_check
|
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
|
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
|
csrf_token_generator: security.csrf.token_manager
|
||||||
|
|
||||||
logout:
|
logout:
|
||||||
# The route name the user can go to in order to logout
|
|
||||||
path: fos_user_security_logout
|
path: fos_user_security_logout
|
||||||
# The name of the route to redirect to after logging out
|
|
||||||
target: homepage
|
target: homepage
|
||||||
|
|
||||||
role_hierarchy:
|
role_hierarchy:
|
||||||
@@ -60,4 +48,4 @@ security:
|
|||||||
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
|
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
|
||||||
|
- { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED }
|
||||||
|
|||||||
@@ -11,15 +11,3 @@ homepage:
|
|||||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||||
route: timesheet
|
route: timesheet
|
||||||
permanent: true
|
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"
|
|
||||||
|
|||||||
@@ -6,3 +6,8 @@ controllers:
|
|||||||
_locale: '%app_locales%'
|
_locale: '%app_locales%'
|
||||||
defaults:
|
defaults:
|
||||||
_locale: '%locale%'
|
_locale: '%locale%'
|
||||||
|
|
||||||
|
app.api:
|
||||||
|
resource: '../src/API/'
|
||||||
|
type: rest
|
||||||
|
prefix: /api
|
||||||
|
|||||||
14
config/routes/fos_user.yaml
Normal file
14
config/routes/fos_user.yaml
Normal file
@@ -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"
|
||||||
11
config/routes/nelmio_api_doc.yaml
Normal file
11
config/routes/nelmio_api_doc.yaml
Normal file
@@ -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 }
|
||||||
18
config/serializer/App/Entity.Activity.yml
Normal file
18
config/serializer/App/Entity.Activity.yml
Normal file
@@ -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
|
||||||
35
config/serializer/App/Entity.Customer.yml
Normal file
35
config/serializer/App/Entity.Customer.yml
Normal file
@@ -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
|
||||||
22
config/serializer/App/Entity.Project.yml
Normal file
22
config/serializer/App/Entity.Project.yml
Normal file
@@ -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
|
||||||
11
config/serializer/App/Entity.User.yml
Normal file
11
config/serializer/App/Entity.User.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
App\Entity\User:
|
||||||
|
exclusion_policy: All
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
include: true
|
||||||
|
alias:
|
||||||
|
include: true
|
||||||
|
title:
|
||||||
|
include: true
|
||||||
|
avatar:
|
||||||
|
include: true
|
||||||
23
config/serializer/FOS/UserBundle/Model.User.yml
Normal file
23
config/serializer/FOS/UserBundle/Model.User.yml
Normal file
@@ -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
|
||||||
@@ -104,3 +104,27 @@ services:
|
|||||||
|
|
||||||
App\Timesheet\Calculator\RateCalculator:
|
App\Timesheet\Calculator\RateCalculator:
|
||||||
arguments: ["%kimai.timesheet.rates%"]
|
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']
|
||||||
|
|||||||
@@ -2,8 +2,4 @@ services:
|
|||||||
_defaults:
|
_defaults:
|
||||||
public: true
|
public: true
|
||||||
|
|
||||||
# If you need to access services in a test, create an alias
|
test.PasswordEncoder: "@security.encoder_factory"
|
||||||
# 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'
|
|
||||||
89
src/API/ActivityController.php
Normal file
89
src/API/ActivityController.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\API;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use App\Repository\ActivityRepository;
|
||||||
|
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||||
|
use FOS\RestBundle\View\View;
|
||||||
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
|
use Nelmio\ApiDocBundle\Annotation as API;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @RouteResource("Activity")
|
||||||
|
*
|
||||||
|
* @Security("is_granted('ROLE_USER')")
|
||||||
|
*/
|
||||||
|
class ActivityController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ActivityRepository
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ViewHandlerInterface
|
||||||
|
*/
|
||||||
|
protected $viewHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ViewHandlerInterface $viewHandler
|
||||||
|
* @param ActivityRepository $repository
|
||||||
|
*/
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/API/CustomerController.php
Normal file
89
src/API/CustomerController.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\API;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Repository\CustomerRepository;
|
||||||
|
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||||
|
use FOS\RestBundle\View\View;
|
||||||
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
|
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @RouteResource("Customer")
|
||||||
|
*
|
||||||
|
* @Security("is_granted('ROLE_USER')")
|
||||||
|
*/
|
||||||
|
class CustomerController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CustomerRepository
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ViewHandlerInterface
|
||||||
|
*/
|
||||||
|
protected $viewHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ViewHandlerInterface $viewHandler
|
||||||
|
* @param CustomerRepository $repository
|
||||||
|
*/
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/API/HealthcheckController.php
Normal file
49
src/API/HealthcheckController.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\API;
|
||||||
|
|
||||||
|
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||||
|
use FOS\RestBundle\View\View;
|
||||||
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
|
||||||
|
class HealthcheckController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ViewHandlerInterface
|
||||||
|
*/
|
||||||
|
protected $viewHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ViewHandlerInterface $viewHandler
|
||||||
|
*/
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/API/NotFoundException.php
Normal file
22
src/API/NotFoundException.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\API;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
class NotFoundException extends NotFoundHttpException
|
||||||
|
{
|
||||||
|
public function __construct(string $message = 'Not found', \Exception $previous = null, int $code = 0, array $headers = [])
|
||||||
|
{
|
||||||
|
parent::__construct($message, $previous, $code, $headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/API/ProjectController.php
Normal file
89
src/API/ProjectController.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\API;
|
||||||
|
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
|
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||||
|
use FOS\RestBundle\View\View;
|
||||||
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
|
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @RouteResource("Project")
|
||||||
|
*
|
||||||
|
* @Security("is_granted('ROLE_USER')")
|
||||||
|
*/
|
||||||
|
class ProjectController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ProjectRepository
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ViewHandlerInterface
|
||||||
|
*/
|
||||||
|
protected $viewHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ViewHandlerInterface $viewHandler
|
||||||
|
* @param ProjectRepository $repository
|
||||||
|
*/
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/API/UserController.php
Normal file
90
src/API/UserController.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||||
|
use FOS\RestBundle\View\View;
|
||||||
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
|
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @RouteResource("User")
|
||||||
|
*
|
||||||
|
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||||
|
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||||
|
*/
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var UserRepository
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ViewHandlerInterface
|
||||||
|
*/
|
||||||
|
protected $viewHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ViewHandlerInterface $viewHandler
|
||||||
|
* @param UserRepository $repository
|
||||||
|
*/
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
268
src/Calendar/TimesheetEntity.php
Normal file
268
src/Calendar/TimesheetEntity.php
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Calendar;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
|
class TimesheetEntity
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $id;
|
||||||
|
/**
|
||||||
|
* @var \DateTime
|
||||||
|
*/
|
||||||
|
protected $start;
|
||||||
|
/**
|
||||||
|
* @var \DateTime|null
|
||||||
|
*/
|
||||||
|
protected $end;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $title;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $customer;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $project;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $activity;
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected $borderColor;
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected $backgroundColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Timesheet $entry
|
||||||
|
*/
|
||||||
|
public function __construct(Timesheet $entry)
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Calendar\Service;
|
use App\Calendar\Service;
|
||||||
|
use App\Calendar\TimesheetEntity;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||||
@@ -96,35 +97,9 @@ class CalendarController extends AbstractController
|
|||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
foreach ($entries as $entry) {
|
foreach ($entries as $entry) {
|
||||||
$result[] = $this->getTimesheetEntryForCalendar($entry);
|
$result[] = new TimesheetEntity($entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->json($result);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Form\UserApiTokenType;
|
||||||
use App\Form\UserEditType;
|
use App\Form\UserEditType;
|
||||||
use App\Form\UserPasswordType;
|
use App\Form\UserPasswordType;
|
||||||
use App\Form\UserPreferencesForm;
|
use App\Form\UserPreferencesForm;
|
||||||
@@ -104,6 +105,32 @@ class ProfileController extends AbstractController
|
|||||||
return $this->getProfileView($profile, 'password', null, $form);
|
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")
|
* @Route("/{username}/roles", name="user_profile_roles")
|
||||||
* @Method({"GET", "POST"})
|
* @Method({"GET", "POST"})
|
||||||
@@ -179,6 +206,7 @@ class ProfileController extends AbstractController
|
|||||||
* @param Form|null $pwdForm
|
* @param Form|null $pwdForm
|
||||||
* @param Form|null $rolesForm
|
* @param Form|null $rolesForm
|
||||||
* @param Form|null $prefsForm
|
* @param Form|null $prefsForm
|
||||||
|
* @param Form|null $apiTokenForm
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||||
*/
|
*/
|
||||||
@@ -188,7 +216,8 @@ class ProfileController extends AbstractController
|
|||||||
Form $editForm = null,
|
Form $editForm = null,
|
||||||
Form $pwdForm = null,
|
Form $pwdForm = null,
|
||||||
Form $rolesForm = null,
|
Form $rolesForm = null,
|
||||||
Form $prefsForm = null
|
Form $prefsForm = null,
|
||||||
|
Form $apiTokenForm = null
|
||||||
) {
|
) {
|
||||||
/* @var $timesheetRepo TimesheetRepository */
|
/* @var $timesheetRepo TimesheetRepository */
|
||||||
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
|
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||||
@@ -211,6 +240,10 @@ class ProfileController extends AbstractController
|
|||||||
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
|
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
|
||||||
$viewVars['forms']['password'] = $pwdForm->createView();
|
$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)) {
|
if ($this->isGranted(UserVoter::ROLES, $user)) {
|
||||||
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
||||||
$viewVars['forms']['roles'] = $rolesForm->createView();
|
$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'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
|||||||
class UserFixtures extends Fixture
|
class UserFixtures extends Fixture
|
||||||
{
|
{
|
||||||
public const DEFAULT_PASSWORD = 'kitten';
|
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 DEFAULT_AVATAR = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y';
|
||||||
|
|
||||||
public const USERNAME_USER = 'john_user';
|
public const USERNAME_USER = 'john_user';
|
||||||
@@ -84,6 +85,7 @@ class UserFixtures extends Fixture
|
|||||||
->setAvatar($userData[5])
|
->setAvatar($userData[5])
|
||||||
->setEnabled($userData[6])
|
->setEnabled($userData[6])
|
||||||
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
||||||
|
->setApiToken($passwordEncoder->encodePassword($user, self::DEFAULT_API_TOKEN))
|
||||||
->setPreferences([$this->getUserPreference($user)])
|
->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
|
* @param ObjectManager $manager
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Doctrine;
|
|||||||
|
|
||||||
use Doctrine\Common\EventSubscriber;
|
use Doctrine\Common\EventSubscriber;
|
||||||
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
|
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a prefix to every doctrine entity AKA database table
|
* Adds a prefix to every doctrine entity AKA database table
|
||||||
@@ -19,16 +20,27 @@ class TablePrefixSubscriber implements EventSubscriber
|
|||||||
{
|
{
|
||||||
protected $prefix = '';
|
protected $prefix = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $prefix
|
||||||
|
*/
|
||||||
public function __construct($prefix)
|
public function __construct($prefix)
|
||||||
{
|
{
|
||||||
$this->prefix = (string) $prefix;
|
$this->prefix = (string) $prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|string[]
|
||||||
|
*/
|
||||||
public function getSubscribedEvents()
|
public function getSubscribedEvents()
|
||||||
{
|
{
|
||||||
return ['loadClassMetadata'];
|
return [
|
||||||
|
Events::loadClassMetadata,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param LoadClassMetadataEventArgs $args
|
||||||
|
*/
|
||||||
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
|
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
|
||||||
{
|
{
|
||||||
$classMetadata = $args->getClassMetadata();
|
$classMetadata = $args->getClassMetadata();
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ class User extends BaseUser implements UserInterface
|
|||||||
*/
|
*/
|
||||||
private $avatar;
|
private $avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="api_token", type="string", length=255, nullable=true)
|
||||||
|
*/
|
||||||
|
protected $apiToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $plainApiToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var UserPreference[]|Collection
|
* @var UserPreference[]|Collection
|
||||||
*
|
*
|
||||||
@@ -179,6 +191,44 @@ class User extends BaseUser implements UserInterface
|
|||||||
return $this;
|
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
|
* @return UserPreference[]|Collection
|
||||||
*/
|
*/
|
||||||
|
|||||||
50
src/Form/UserApiTokenType.php
Normal file
50
src/Form/UserApiTokenType.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the form used to set the users API token.
|
||||||
|
*/
|
||||||
|
class UserApiTokenType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
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
|
class UserPasswordType extends AbstractType
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use Doctrine\DBAL\Schema\Schema;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial database structure of Kimai 2.
|
* 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
|
final class Version20180701120000 extends AbstractMigration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ use Doctrine\DBAL\Schema\Index;
|
|||||||
use Doctrine\DBAL\Schema\Schema;
|
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
|
final class Version20180730044139 extends AbstractMigration
|
||||||
{
|
{
|
||||||
|
|||||||
79
src/Migrations/Version20180805183527.php
Normal file
79
src/Migrations/Version20180805183527.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use App\Doctrine\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Added "API-token" to users table.
|
||||||
|
*/
|
||||||
|
final class Version20180805183527 extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
* @throws \Doctrine\DBAL\DBALException
|
||||||
|
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$platform = $this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/Security/TokenAuthenticator.php
Normal file
164
src/Security/TokenAuthenticator.php
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
|
||||||
|
|
||||||
|
class TokenAuthenticator extends AbstractGuardAuthenticator
|
||||||
|
{
|
||||||
|
public const HEADER_USERNAME = 'X-AUTH-USER';
|
||||||
|
public const HEADER_TOKEN = 'X-AUTH-TOKEN';
|
||||||
|
public const HEADER_JAVASCRIPT = 'X-AUTH-SESSION';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EncoderFactoryInterface
|
||||||
|
*/
|
||||||
|
protected $encoderFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param EncoderFactoryInterface $encoderFactory
|
||||||
|
*/
|
||||||
|
public function __construct(EncoderFactoryInterface $encoderFactory)
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ class UserVoter extends AbstractVoter
|
|||||||
public const PASSWORD = 'password';
|
public const PASSWORD = 'password';
|
||||||
public const ROLES = 'roles';
|
public const ROLES = 'roles';
|
||||||
public const PREFERENCES = 'preferences';
|
public const PREFERENCES = 'preferences';
|
||||||
|
public const API_TOKEN = 'api-token';
|
||||||
|
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
public const ALLOWED_ATTRIBUTES = [
|
||||||
self::VIEW,
|
self::VIEW,
|
||||||
@@ -32,7 +33,8 @@ class UserVoter extends AbstractVoter
|
|||||||
self::ROLES,
|
self::ROLES,
|
||||||
self::PASSWORD,
|
self::PASSWORD,
|
||||||
self::DELETE,
|
self::DELETE,
|
||||||
self::PREFERENCES
|
self::PREFERENCES,
|
||||||
|
self::API_TOKEN,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +73,7 @@ class UserVoter extends AbstractVoter
|
|||||||
case self::VIEW:
|
case self::VIEW:
|
||||||
return $this->canView($subject, $user, $token);
|
return $this->canView($subject, $user, $token);
|
||||||
case self::EDIT:
|
case self::EDIT:
|
||||||
|
case self::API_TOKEN:
|
||||||
case self::PASSWORD:
|
case self::PASSWORD:
|
||||||
return $this->canEdit($subject, $user, $token);
|
return $this->canEdit($subject, $user, $token);
|
||||||
case self::DELETE:
|
case self::DELETE:
|
||||||
|
|||||||
57
symfony.lock
57
symfony.lock
@@ -110,6 +110,9 @@
|
|||||||
"erusev/parsedown": {
|
"erusev/parsedown": {
|
||||||
"version": "1.6.4"
|
"version": "1.6.4"
|
||||||
},
|
},
|
||||||
|
"exsyst/swagger": {
|
||||||
|
"version": "v0.4.1"
|
||||||
|
},
|
||||||
"friendsofphp/php-cs-fixer": {
|
"friendsofphp/php-cs-fixer": {
|
||||||
"version": "2.2",
|
"version": "2.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@@ -119,6 +122,15 @@
|
|||||||
"ref": "bb31a3bbec00a8fc8aa1c9fbf9b0ef9fc492f93d"
|
"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": {
|
"friendsofsymfony/user-bundle": {
|
||||||
"version": "v2.1.2"
|
"version": "v2.1.2"
|
||||||
},
|
},
|
||||||
@@ -128,6 +140,24 @@
|
|||||||
"jdorn/sql-formatter": {
|
"jdorn/sql-formatter": {
|
||||||
"version": "v1.2.17"
|
"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": {
|
"kevinpapst/adminlte-bundle": {
|
||||||
"version": "0.3"
|
"version": "0.3"
|
||||||
},
|
},
|
||||||
@@ -137,6 +167,15 @@
|
|||||||
"myclabs/deep-copy": {
|
"myclabs/deep-copy": {
|
||||||
"version": "1.7.0"
|
"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": {
|
"nikic/php-parser": {
|
||||||
"version": "v4.0.2"
|
"version": "v4.0.2"
|
||||||
},
|
},
|
||||||
@@ -161,6 +200,9 @@
|
|||||||
"php-cs-fixer/diff": {
|
"php-cs-fixer/diff": {
|
||||||
"version": "v1.2.0"
|
"version": "v1.2.0"
|
||||||
},
|
},
|
||||||
|
"phpcollection/phpcollection": {
|
||||||
|
"version": "0.5.0"
|
||||||
|
},
|
||||||
"phpdocumentor/reflection-common": {
|
"phpdocumentor/reflection-common": {
|
||||||
"version": "1.0.1"
|
"version": "1.0.1"
|
||||||
},
|
},
|
||||||
@@ -170,6 +212,9 @@
|
|||||||
"phpdocumentor/type-resolver": {
|
"phpdocumentor/type-resolver": {
|
||||||
"version": "0.4.0"
|
"version": "0.4.0"
|
||||||
},
|
},
|
||||||
|
"phpoption/phpoption": {
|
||||||
|
"version": "1.5.0"
|
||||||
|
},
|
||||||
"phpspec/prophecy": {
|
"phpspec/prophecy": {
|
||||||
"version": "1.7.3"
|
"version": "1.7.3"
|
||||||
},
|
},
|
||||||
@@ -413,6 +458,9 @@
|
|||||||
"symfony/property-access": {
|
"symfony/property-access": {
|
||||||
"version": "v4.0.3"
|
"version": "v4.0.3"
|
||||||
},
|
},
|
||||||
|
"symfony/property-info": {
|
||||||
|
"version": "v4.1.3"
|
||||||
|
},
|
||||||
"symfony/routing": {
|
"symfony/routing": {
|
||||||
"version": "4.0",
|
"version": "4.0",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@@ -536,10 +584,19 @@
|
|||||||
"white-october/pagerfanta-bundle": {
|
"white-october/pagerfanta-bundle": {
|
||||||
"version": "v1.1.2"
|
"version": "v1.1.2"
|
||||||
},
|
},
|
||||||
|
"willdurand/jsonp-callback-validator": {
|
||||||
|
"version": "v1.1.0"
|
||||||
|
},
|
||||||
|
"willdurand/negotiation": {
|
||||||
|
"version": "v2.3.1"
|
||||||
|
},
|
||||||
"zendframework/zend-code": {
|
"zendframework/zend-code": {
|
||||||
"version": "3.3.0"
|
"version": "3.3.0"
|
||||||
},
|
},
|
||||||
"zendframework/zend-eventmanager": {
|
"zendframework/zend-eventmanager": {
|
||||||
"version": "3.2.0"
|
"version": "3.2.0"
|
||||||
|
},
|
||||||
|
"zircote/swagger-php": {
|
||||||
|
"version": "2.0.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ swagger_data.spec.info.title }}</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700">
|
||||||
|
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/style.css') }}">
|
||||||
|
{# json_encode(65) is for JSON_UNESCAPED_SLASHES|JSON_HEX_TAG to avoid JS XSS #}
|
||||||
|
<script id="swagger-data" type="application/json">{{ swagger_data|json_encode(65)|raw }}</script>
|
||||||
|
</head>
|
||||||
|
<body style="margin-top:0;">
|
||||||
|
<div id="swagger-ui" class="api-platform"></div>
|
||||||
|
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-bundle.js') }}"></script>
|
||||||
|
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-standalone-preset.js') }}"></script>
|
||||||
|
<script src="{{ asset('bundles/nelmioapidoc/init-swagger-ui.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
169
tests/API/APIControllerBaseTest.php
Normal file
169
tests/API/APIControllerBaseTest.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\API;
|
||||||
|
|
||||||
|
use App\DataFixtures\UserFixtures;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Tests\Controller\ControllerBaseTest;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds some useful functions for writing API integration tests.
|
||||||
|
*/
|
||||||
|
abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $role
|
||||||
|
* @return Client
|
||||||
|
*/
|
||||||
|
protected function getClientForAuthenticatedUser(string $role = User::ROLE_USER)
|
||||||
|
{
|
||||||
|
switch ($role) {
|
||||||
|
case User::ROLE_SUPER_ADMIN:
|
||||||
|
$client = self::createClient([], [
|
||||||
|
'HTTP_X_AUTH_USER' => 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
tests/API/ActivityControllerTest.php
Normal file
64
tests/API/ActivityControllerTest.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \App\API\ActivityController
|
||||||
|
* @group integration
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
class ActivityControllerTest extends APIControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests/API/ApiDocControllerTest.php
Normal file
51
tests/API/ApiDocControllerTest.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Tests\Controller\ControllerBaseTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group integration
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
class ApiDocControllerTest extends ControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->assertUrlIsSecured('/api/doc');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetDocs()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
|
$this->assertAccessIsGranted($client, '/api/doc');
|
||||||
|
$this->assertContains('<title>Kimai 2 - API Docs</title>', $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, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
65
tests/API/CustomerControllerTest.php
Normal file
65
tests/API/CustomerControllerTest.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \App\API\CustomerController
|
||||||
|
* @group integration
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
class CustomerControllerTest extends APIControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
tests/API/HealthcheckControllerTest.php
Normal file
35
tests/API/HealthcheckControllerTest.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \App\API\HealthcheckController
|
||||||
|
* @group integration
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
class HealthcheckControllerTest extends APIControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
tests/API/ProjectControllerTest.php
Normal file
64
tests/API/ProjectControllerTest.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \App\API\ProjectController
|
||||||
|
* @group integration
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
class ProjectControllerTest extends APIControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
65
tests/API/UserControllerTest.php
Normal file
65
tests/API/UserControllerTest.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \App\API\UserController
|
||||||
|
* @group integration
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
class UserControllerTest extends APIControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,15 +52,13 @@ class UserControllerTest extends ControllerBaseTest
|
|||||||
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit'));
|
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit'));
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
|
|
||||||
|
$expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#roles'];
|
||||||
|
|
||||||
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
||||||
$this->assertEquals(4, $tabs->count());
|
$this->assertEquals(count($expectedTabs), $tabs->count());
|
||||||
$expectedTabs = ['#charts', '#settings', '#password', '#roles'];
|
|
||||||
$foundTabs = [];
|
$foundTabs = [];
|
||||||
foreach ($tabs->filter('a') as $tab) {
|
foreach ($tabs->filter('a') as $tab) {
|
||||||
$name = $tab->getAttribute('href');
|
$foundTabs[] = $tab->getAttribute('href');
|
||||||
if (in_array($name, $expectedTabs)) {
|
|
||||||
$foundTabs[] = $name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$this->assertEmpty(array_diff($expectedTabs, $foundTabs));
|
$this->assertEmpty(array_diff($expectedTabs, $foundTabs));
|
||||||
|
|
||||||
|
|||||||
@@ -72,5 +72,27 @@ class CalendarControllerTest extends ControllerBaseTest
|
|||||||
$this->assertInternalType('array', $json);
|
$this->assertInternalType('array', $json);
|
||||||
$this->assertNotEmpty($json);
|
$this->assertNotEmpty($json);
|
||||||
$this->assertEquals(10, count($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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,18 +96,18 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
*/
|
*/
|
||||||
protected function assertRequestIsSecured(Client $client, string $url, $method = 'GET')
|
protected function assertRequestIsSecured(Client $client, string $url, $method = 'GET')
|
||||||
{
|
{
|
||||||
$client->request($method, $this->createUrl($url));
|
$this->request($client, $url, $method);
|
||||||
|
|
||||||
/* @var RedirectResponse $response */
|
/* @var RedirectResponse $response */
|
||||||
$response = $client->getResponse();
|
$response = $client->getResponse();
|
||||||
|
|
||||||
$this->assertTrue(
|
$this->assertTrue(
|
||||||
$response->isRedirect(),
|
$response->isRedirect(),
|
||||||
sprintf('The secure URL %s is not protected.', $url . $response->getContent())
|
sprintf('The secure URL %s is not protected.', $url)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertEquals(
|
$this->assertStringEndsWith(
|
||||||
'http://localhost' . $this->createUrl('/login'),
|
'/login',
|
||||||
$response->getTargetUrl(),
|
$response->getTargetUrl(),
|
||||||
sprintf('The secure URL %s does not redirect to the login form.', $url)
|
sprintf('The secure URL %s does not redirect to the login form.', $url)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,10 +10,13 @@
|
|||||||
namespace App\Tests\Controller;
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
use App\DataFixtures\UserFixtures;
|
use App\DataFixtures\UserFixtures;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @coversDefaultClass \App\Controller\InvoiceController
|
* @coversDefaultClass \App\Controller\InvoiceController
|
||||||
* @group integration
|
* @group integration
|
||||||
|
* @group legacy
|
||||||
*/
|
*/
|
||||||
class ProfileControllerTest extends ControllerBaseTest
|
class ProfileControllerTest extends ControllerBaseTest
|
||||||
{
|
{
|
||||||
@@ -28,15 +31,13 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER);
|
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER);
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#preferences'];
|
||||||
|
|
||||||
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
||||||
$this->assertEquals(4, $tabs->count());
|
$this->assertEquals(count($expectedTabs), $tabs->count());
|
||||||
$expectedTabs = ['#charts', '#settings', '#password', '#preferences'];
|
|
||||||
$foundTabs = [];
|
$foundTabs = [];
|
||||||
foreach ($tabs->filter('a') as $tab) {
|
foreach ($tabs->filter('a') as $tab) {
|
||||||
$name = $tab->getAttribute('href');
|
$foundTabs[] = $tab->getAttribute('href');
|
||||||
if (in_array($name, $expectedTabs)) {
|
|
||||||
$foundTabs[] = $name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$this->assertEmpty(array_diff($expectedTabs, $foundTabs));
|
$this->assertEmpty(array_diff($expectedTabs, $foundTabs));
|
||||||
}
|
}
|
||||||
@@ -47,4 +48,74 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_TEAMLEAD);
|
$this->request($client, '/profile/' . UserFixtures::USERNAME_TEAMLEAD);
|
||||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
$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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,14 @@
|
|||||||
<source>label.user_profile</source>
|
<source>label.user_profile</source>
|
||||||
<target>Mein Profil</target>
|
<target>Mein Profil</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="label.api_token">
|
||||||
|
<source>label.api_token</source>
|
||||||
|
<target>API Passwort</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.api_token_repeat">
|
||||||
|
<source>label.api_token_repeat</source>
|
||||||
|
<target>API Passwort wiederholen</target>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Menu / Navbar items
|
Menu / Navbar items
|
||||||
@@ -343,6 +351,10 @@
|
|||||||
<source>profile.password</source>
|
<source>profile.password</source>
|
||||||
<target>Passwort</target>
|
<target>Passwort</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="profile.api-token">
|
||||||
|
<source>profile.api-token</source>
|
||||||
|
<target>API</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="profile.roles">
|
<trans-unit id="profile.roles">
|
||||||
<source>profile.roles</source>
|
<source>profile.roles</source>
|
||||||
<target>Rollen</target>
|
<target>Rollen</target>
|
||||||
|
|||||||
@@ -63,6 +63,14 @@
|
|||||||
<source>label.user_profile</source>
|
<source>label.user_profile</source>
|
||||||
<target>My profile</target>
|
<target>My profile</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="label.api_token">
|
||||||
|
<source>label.api_token</source>
|
||||||
|
<target>API password</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.api_token_repeat">
|
||||||
|
<source>label.api_token_repeat</source>
|
||||||
|
<target>Repeat API password</target>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Menu / Navbar items
|
Menu / Navbar items
|
||||||
@@ -351,6 +359,10 @@
|
|||||||
<source>profile.password</source>
|
<source>profile.password</source>
|
||||||
<target>Password</target>
|
<target>Password</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="profile.api-token">
|
||||||
|
<source>profile.api-token</source>
|
||||||
|
<target>API</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="profile.roles">
|
<trans-unit id="profile.roles">
|
||||||
<source>profile.roles</source>
|
<source>profile.roles</source>
|
||||||
<target>Roles</target>
|
<target>Roles</target>
|
||||||
|
|||||||
Binary file not shown.
@@ -13,12 +13,16 @@ For the most part Kimai usage should be self-explanatory, so we will only cover
|
|||||||
|
|
||||||
- [Timesheets](timesheet.md) - information about timesheets
|
- [Timesheets](timesheet.md) - information about timesheets
|
||||||
|
|
||||||
## Developers and Administrators
|
## Developer & Administrator
|
||||||
|
|
||||||
- [Kimai configurations](configurations.md) - application configs, which can only be changed in config files
|
- [Kimai configurations](configurations.md) - application configs, which can only be changed in config files
|
||||||
- [Developer docu](developers.md) - how to extend Kimai's feature set
|
- [Developer docu](developers.md) - how to extend Kimai's feature set
|
||||||
- [User and Security](users.md) - docu for user and security topics, like authentication, registration and roles
|
- [User and Security](users.md) - docu for user and security topics, like authentication, registration and roles
|
||||||
- [FAQ](faq.md) - some answers to frequently asked questions
|
- [FAQ](faq.md) - some answers to frequently asked questions
|
||||||
|
- [Emails](emails.md) - transport configuration and handling of emails
|
||||||
|
- [API](developers_api.md) - how to use the JSON API
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
- [Installation](installation.md) - instructions about several installation methods
|
- [Installation](installation.md) - instructions about several installation methods
|
||||||
- [Importing data](migration_v1.md) - how to import your timesheets from Kimai v1
|
- [Importing data](migration_v1.md) - how to import your timesheets from Kimai v1
|
||||||
- [Emails](emails.md) - transport configuration and handling of emails
|
|
||||||
|
|||||||
23
var/docs/developers_api.md
Normal file
23
var/docs/developers_api.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# REST API
|
||||||
|
|
||||||
|
Read the Swagger documentation of the Kimai 2 API in your Kimai installation at `/api/doc`.
|
||||||
|
|
||||||
|
Or you can export the JSON collection by visiting `/api/doc.json`. Store the result in a file, which can be imported with Postman.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
When calling the API you have to submit two additional header with every call for authentication:
|
||||||
|
|
||||||
|
- `X-AUTH-USER` - holds the username or email
|
||||||
|
- `X-AUTH-TOKEN` - holds the users API password, which he can set in his profile
|
||||||
|
|
||||||
|
Please make sure to ONLY call the Kimai 2 API via `https` to protect the users data!
|
||||||
|
|
||||||
|
## Calling the API with Javascript
|
||||||
|
|
||||||
|
If you develop your own extension and need to use the API for logged-in user, then you have to set the header `X-AUTH-SESSION`
|
||||||
|
which will allow Kimai to use the current user session and not look for the default token based API authentication.
|
||||||
|
|
||||||
|
### Next step
|
||||||
|
|
||||||
|
Back to the [developer documentation](developers.md).
|
||||||
@@ -10,3 +10,4 @@ Internal documentation for project maintainers
|
|||||||
- Push a release branch and add it as last PR merge into master
|
- Push a release branch and add it as last PR merge into master
|
||||||
- Edit the release-draft and add the "Full changelog" link + everything from the "Merged pull requests" section from CHANGELOG.md
|
- Edit the release-draft and add the "Full changelog" link + everything from the "Merged pull requests" section from CHANGELOG.md
|
||||||
- Create the release
|
- Create the release
|
||||||
|
- Post a new issue at [YunoHost tracker for Kimai 2](https://github.com/YunoHost-Apps/kimai2_ynh)
|
||||||
Reference in New Issue
Block a user