Integrated FOSUserBundle (#216)

This commit is contained in:
Kevin Papst
2018-07-21 22:36:36 +02:00
committed by GitHub
parent 2d2525cff2
commit 75246e9db2
77 changed files with 1413 additions and 514 deletions

View File

@@ -4,6 +4,7 @@
# KIMAI DEFAULT ENV VARS
DATABASE_PREFIX=kimai2_
MAILER_FROM=kimai@example.com
###> symfony/framework-bundle ###
APP_ENV=dev

View File

@@ -11,14 +11,9 @@ build:
tests:
override:
-
command: 'vendor/bin/phpunit --exclude-group integration --coverage-clover=unit tests/'
command: 'vendor/bin/phpunit --coverage-clover=clover.xml tests/'
coverage:
file: 'unit'
format: 'clover'
-
command: 'vendor/bin/phpunit --group integration --coverage-clover=integration tests/'
coverage:
file: 'integration'
file: 'clover.xml'
format: 'clover'
-
command: 'bin/console kimai:phpcs --checkstyle=checkstyle'

View File

@@ -5,6 +5,12 @@ cache:
directories:
- $HOME/.composer/cache/files
addons:
mariadb: '10.2'
services:
- mysql
matrix:
fast_finish: true
include:
@@ -19,5 +25,10 @@ install:
- composer install
script:
- vendor/bin/phpunit --exclude-group integration tests/
- vendor/bin/phpunit --group integration tests/
- vendor/bin/phpunit tests/
- cp tests/.env.dist.sqlite .env
- bin/console doctrine:database:create -n
- bin/console doctrine:migrations:migrate -n
- cp tests/.env.dist.mysql .env
- bin/console doctrine:database:create -n
- bin/console doctrine:migrations:migrate -n

View File

@@ -59,27 +59,40 @@ chmod -R 777 var/
```
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite.
Create your database and configure the connection string in your environment, e.g. with the `.env` file (more examples in `.env.dist`):
Configure the database connection string in your the `.env` file:
```
# adjust all settings in .env to your needs
APP_ENV=prod
APP_SECRET=insert_a_random_secret_string_for_production
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
```
After activating `prod` environment you can prepare the environment by installing all dependencies:
Now install all dependencies for Kimai 2:
```bash
composer install --no-dev
```
Create the database schemas and warm up the cache (as webserver user):
Optionally create the database:
```bash
bin/console doctrine:database:create
```
Create all schema tables:
```bash
bin/console doctrine:schema:create
```
Make sure that upcoming update can be automatically applied by setting the correct database version:
```bash
bin/console doctrine:migrations:version --add --all
```
Warm up the cache (as webserver user):
```bash
sudo -u www-data bin/console cache:warmup --env=prod
```
Create your first user with the following command. You will be asked to enter a password afterwards.
Create your first user with the following command. You will be asked to enter a password afterwards:
```bash
bin/console kimai:create-user username admin@example.com ROLE_SUPER_ADMIN
```
@@ -94,7 +107,42 @@ For available roles, please refer to the [user documentation](var/docs/users.md)
> For more details, see:
> http://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html
That's it, you can start time-tracking :-)
Installation complete: enjoy time-tracking :-)
## Updating Kimai
**STOP:** Its important that you do not execute the Installation steps before or after your upgrade.
Make sure that you have a working database backup you start with these simple steps:
Get the latest code and install dependencies:
```bash
cd kimai2/
git pull origin master
composer install --no-dev
```
Refresh your cache:
```bash
sudo -u www-data bin/console cache:clear --env=prod
sudo -u www-data bin/console cache:warmup --env=prod
```
And upgrade your database:
```bash
bin/console doctrine:migrations:migrate
```
Done! You can use the latest version of Kimai 2.
## Rolling releases & Git
Please note: Kimai 2 uses a rolling release concept for delivering updates.
Release versions will be created on a regular base and you can use these tags if you are familiar with Git, but we
will not provide support for any specific version (whether its bugs or installation/update docu).
To clarify that further: Every code change, whether its new features or bug fixes, is targeted against master branch and
will be intensively tested before merge. We have to go this way, as we develop Kimai in our free time and want to put our
effort into the software instead of installation scripts and complicated upgrade processes.
## Extensions for Kimai 2

View File

@@ -117,11 +117,6 @@ footer.main-footer {
font-size: 80%;
}
/* ================================ LOGIN-PAGE ================================ */
.login-page form label {
padding-left: 5px;
}
/* ================================ PRINT ================================ */
@media print{

View File

@@ -10,7 +10,8 @@
"ext-pdo_sqlite": "*",
"beberlei/DoctrineExtensions": "^1.0",
"erusev/parsedown": "^1.6",
"kevinpapst/adminlte-bundle": "^1.0",
"friendsofsymfony/user-bundle": "~2.0",
"kevinpapst/adminlte-bundle": "^1.2",
"ocramius/proxy-manager": "2.1.1",
"sensio/framework-extra-bundle": "^5.1",
"symfony/asset": "^4.0",

196
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2ccc40edc01587287a3db23b0e9b0458",
"content-hash": "1ebffbf7579748f5352725fdc5c63676",
"packages": [
{
"name": "beberlei/DoctrineExtensions",
@@ -1093,6 +1093,82 @@
],
"time": "2018-03-08T01:11:30+00:00"
},
{
"name": "friendsofsymfony/user-bundle",
"version": "v2.1.2",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfSymfony/FOSUserBundle.git",
"reference": "1049935edd24ec305cc6cfde1875372fa9600446"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/1049935edd24ec305cc6cfde1875372fa9600446",
"reference": "1049935edd24ec305cc6cfde1875372fa9600446",
"shasum": ""
},
"require": {
"paragonie/random_compat": "^1 || ^2",
"php": "^5.5.9 || ^7.0",
"symfony/form": "^2.8 || ^3.0 || ^4.0",
"symfony/framework-bundle": "^2.8 || ^3.0 || ^4.0",
"symfony/security-bundle": "^2.8 || ^3.0 || ^4.0",
"symfony/templating": "^2.8 || ^3.0 || ^4.0",
"symfony/twig-bundle": "^2.8 || ^3.0 || ^4.0",
"symfony/validator": "^2.8 || ^3.0 || ^4.0",
"twig/twig": "^1.28 || ^2.0"
},
"conflict": {
"doctrine/doctrine-bundle": "<1.3",
"symfony/doctrine-bridge": "<2.7"
},
"require-dev": {
"doctrine/doctrine-bundle": "^1.3",
"friendsofphp/php-cs-fixer": "^2.2",
"phpunit/phpunit": "^4.8.35|^5.7.11|^6.5",
"swiftmailer/swiftmailer": "^4.3 || ^5.0 || ^6.0",
"symfony/console": "^2.8 || ^3.0 || ^4.0",
"symfony/phpunit-bridge": "^2.8 || ^3.0 || ^4.0",
"symfony/yaml": "^2.8 || ^3.0 || ^4.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
}
},
"autoload": {
"psr-4": {
"FOS\\UserBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christophe Coevoet",
"email": "stof@notk.org"
},
{
"name": "FriendsOfSymfony Community",
"homepage": "https://github.com/friendsofsymfony/FOSUserBundle/contributors"
},
{
"name": "Thibault Duplessis"
}
],
"description": "Symfony FOSUserBundle",
"homepage": "http://friendsofsymfony.github.com",
"keywords": [
"User management"
],
"time": "2018-03-08T08:59:27+00:00"
},
{
"name": "jdorn/sql-formatter",
"version": "v1.2.17",
@@ -1145,16 +1221,16 @@
},
{
"name": "kevinpapst/adminlte-bundle",
"version": "1.0",
"version": "1.2",
"source": {
"type": "git",
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
"reference": "a225f58aeba1aace8f3675fcbb23990d3883f15c"
"reference": "e9ba0374f1cb952d9c987b5d2f2fd78274c8d3dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/a225f58aeba1aace8f3675fcbb23990d3883f15c",
"reference": "a225f58aeba1aace8f3675fcbb23990d3883f15c",
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/e9ba0374f1cb952d9c987b5d2f2fd78274c8d3dd",
"reference": "e9ba0374f1cb952d9c987b5d2f2fd78274c8d3dd",
"shasum": ""
},
"require": {
@@ -1173,7 +1249,8 @@
"phpspec/prophecy": "^1.6"
},
"suggest": {
"knplabs/knp-menu-bundle": "Allows easy menu integration."
"friendsofsymfony/user-bundle": "Allows easy user management and security support",
"knplabs/knp-menu-bundle": "Allows easy menu integration"
},
"type": "symfony-bundle",
"autoload": {
@@ -1200,7 +1277,7 @@
}
],
"description": "Admin Theme based on the AdminLTE 2.3 for easy integration with Symfony 4",
"time": "2018-07-12T17:53:22+00:00"
"time": "2018-07-21T18:49:21+00:00"
},
{
"name": "monolog/monolog",
@@ -1467,6 +1544,55 @@
],
"time": "2018-05-17T09:23:52+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v2.0.17",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d",
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"time": "2018-07-04T16:31:37+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",
@@ -4000,6 +4126,62 @@
"homepage": "http://symfony.com",
"time": "2018-04-03T16:29:41+00:00"
},
{
"name": "symfony/templating",
"version": "v4.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/templating.git",
"reference": "c9731b5e11fc01a1363e0cf9b3c90faf619ea479"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/templating/zipball/c9731b5e11fc01a1363e0cf9b3c90faf619ea479",
"reference": "c9731b5e11fc01a1363e0cf9b3c90faf619ea479",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/polyfill-ctype": "~1.8"
},
"require-dev": {
"psr/log": "~1.0"
},
"suggest": {
"psr/log-implementation": "For using debug logging in loaders"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Templating\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Templating Component",
"homepage": "https://symfony.com",
"time": "2018-05-01T23:02:13+00:00"
},
{
"name": "symfony/translation",
"version": "v4.1.1",

View File

@@ -18,4 +18,5 @@ return [
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true],
FOS\UserBundle\FOSUserBundle::class => ['all' => true],
];

View File

@@ -32,4 +32,7 @@ admin_lte:
routes:
adminlte_profile: user_profile
adminlte_welcome: dashboard
adminlte_login: security_login
adminlte_login: fos_user_security_login
adminlte_login_check: fos_user_security_check
adminlte_registration: fos_user_registration_register
adminlte_password_reset: fos_user_resetting_request

View File

@@ -11,6 +11,9 @@ doctrine:
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci
# With Symfony 3.3, remove the `resolve:` prefix
url: '%env(resolve:DATABASE_URL)%'

View File

@@ -0,0 +1,25 @@
fos_user:
db_driver: orm
firewall_name: secured_area
user_class: App\Entity\User
from_email:
address: '%env(MAILER_FROM)%'
sender_name: "Kimai 2"
registration:
confirmation:
enabled: false
template: '@FOSUser/Registration/email.txt.twig'
resetting:
retry_ttl: 7200
token_ttl: 86400
email:
template: '@FOSUser/Resetting/email.txt.twig'
service:
mailer: fos_user.mailer.twig_swift
# group:
# group_class: ~ # Required when using groups
# group_manager: fos_user.group_manager.default
# form:
# type: FOS\UserBundle\Form\Type\GroupFormType # or 'fos_user_group' on Symfony < 2.8
# name: fos_user_group_form
# validation_groups: [Registration, Default]

View File

@@ -1,4 +1,5 @@
kimai:
# All configs related to timesheet and record management
timesheet:

View File

@@ -4,9 +4,8 @@ security:
# https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
database_users:
entity:
class: App\Entity\User
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
dev:
@@ -32,18 +31,20 @@ security:
# This allows the user to login by submitting a username and password
# Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
form_login:
# fos user bundle handles the form login
provider: fos_userbundle
# The route name that the login form submits to
check_path: security_login
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: 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
logout:
# The route name the user can go to in order to logout
path: security_logout
path: fos_user_security_logout
# The name of the route to redirect to after logging out
target: homepage
@@ -54,9 +55,9 @@ security:
ROLE_SUPER_ADMIN: ROLE_ADMIN
access_control:
# this is a catch-all for the admin area
# additional security lives in the controllers
- { path: '^/(%app_locales%)$', role: IS_AUTHENTICATED_FULLY }
- { path: '^/(%app_locales%)$', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/login', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }

View File

@@ -11,3 +11,15 @@ homepage:
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
route: timesheet
permanent: true
fos_user_security:
prefix: /{_locale}
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
fos_user_registration:
prefix: /{_locale}/register
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
fos_user_resetting:
prefix: /{_locale}/resetting
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"

View File

@@ -0,0 +1,13 @@
App\Entity\User:
properties:
roles:
- App\Validator\Constraints\Role: ~
username:
- NotBlank: ~
- Length: { min: 5, max: 60 }
email:
- NotBlank: ~
- Email: ~
plainPassword:
- NotBlank: { groups: [registration, passwordUpdate] }
- Length: { min: 5, max: 60, groups: [registration, passwordUpdate] }

View File

@@ -7,11 +7,12 @@
"prod": "node_modules/.bin/encore production"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.1.0",
"@fortawesome/fontawesome-free": "~5.1.1",
"@symfony/webpack-encore": "^0.20.1",
"admin-lte": "^2.3.11",
"admin-lte": "~2.3.11",
"bootstrap-sass": "^3.3.7",
"bootstrap-select": "^1.13.1",
"chart.js": "1.1.*",
"daterangepicker": "^3.0.3",
"icheck": "^1.0.2",
"jquery": "^3.3.1",

View File

@@ -10,7 +10,7 @@
<php>
<ini name="error_reporting" value="-1" />
<ini name="intl.default_locale" value="en_US" />
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
<env name="APP_ENV" value="test" force="true"/>
<env name="APP_DEBUG" value="1" force="true"/>
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
@@ -37,8 +37,12 @@
<directory suffix=".php">src/</directory>
<directory suffix=".php">templates/</directory>
<exclude>
<directory suffix=".php">assets/</directory>
<directory suffix=".php">bin/</directory>
<directory suffix=".php">config/</directory>
<directory suffix=".php">public/</directory>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">translations/</directory>
<directory suffix=".php">var/</directory>
<directory suffix=".php">vendor/</directory>
</exclude>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -819,6 +819,9 @@
<glyph glyph-name="resolving"
unicode="&#xF3E7;"
horiz-adv-x="496" d=" M281.2 169.8C327.2 183.1 330.8 193.3 325.2 213.2L314 252.5C307.9 273.4 295.6 280.6 242.9 265.3L54.7 211.2L83.3 112.6L281.2000000000001 169.8zM248.5 440C131.4 440 33.2 359.3 7.2 250.5L229.1 314.4C263.9 324.6 283.3 326.1 308.4 322.6C344.7 316.5 361.1 297.6 369.8 267.4L380.5 229.6C388.7 201.5 381.5 179 357 156C337.6 138.6 325.8 131.5 295.6 122.8L203 96.2L423.4 69.1L433.1 34.9L385 21.6L98.1999999999999 58.9L121.1999999999999 -21.3C158 -43.3 201.5 -56 247.5 -56C384.5 -56 495.9999999999999 55.4 495.9999999999999 192.3C497 328.6 385.5 440 248.5 440zM38.3 59.4L0 191.2C0 142.7 14.3 97.8 38.3 59.4z" />
<glyph glyph-name="rev"
unicode="&#xF5B2;"
horiz-adv-x="410.22" d=" M270.67 173.11C270.67 136.9500000000001 241.26 107.54 205.11 107.54S139.54 136.9500000000001 139.54 173.11S168.95 238.67 205.11 238.67S270.67 209.27 270.67 173.11zM410.2200000000001 178.16H410.0900000000001C408.6 239.7100000000001 379.99 294.51 335.7700000000001 331.14L290.3900000000001 304.9400000000001C333.5600000000001 276.9100000000001 362.2000000000001 228.3100000000001 362.2000000000001 173.12C362.2000000000001 86.5 291.73 16.03 205.1100000000001 16.03S48.02 86.5 48.02 173.12C48.02 254.98 110.98 322.39 191.01 329.55V290.43L299.7700000000001 353.2200000000001L191.01 416V377.68C84.31 370.43 0 281.64 0 173.11C0 61.52 89.12 -29.1799999999999 200.06 -31.87V-32H410.2200000000001V178.16z" />
<glyph glyph-name="rocketchat"
unicode="&#xF3E8;"
horiz-adv-x="448" d=" M448 191.8C448 279 348.4 345.1 228.2 345.1C209.4 345.1 190.9 343.5 172.9 340.3C161.8 350.8 148.7 360.3 134.9 367.7000000000001C61.2 403.8 0 368.6 0 368.6S56.9 321.5 47.6 280.3C-4.7 228 -4.9 156.2000000000001 47.6 103.7000000000001C56.9 62.4 0 15.4 0 15.4S61.2 -19.8 134.9 16.2C148.7 23.7 161.8 33.1 172.9 43.6C190.9 40.4 209.4 38.8 228.2 38.8C348.5 38.9 448 104.6 448 191.8zM228.3 67.8C204.6 67.8 182 70.6 161 75.6C139.7 49.8 92.9 13.9 47.4 25.5C62.2 41.5 84.1 68.6 79.4 113.1C52.1 134.5 35.8 161.8 35.8 191.6C35.8 260 122 315.5 228.3 315.5S420.8 260 420.8 191.6C420.8 123.3 334.6 67.8 228.3 67.8zM253.9 191.7000000000001C253.9 177.5000000000001 242.4 165.9 228.3 165.9C214.2 165.9 202.7 177.4 202.7 191.7000000000001C202.7 205.9 214.2 217.5000000000001 228.3 217.5000000000001C242.4 217.5000000000001 253.9 205.9000000000001 253.9 191.7000000000001zM342.8 191.7000000000001C342.8 177.5000000000001 331.4000000000001 165.9 317.2 165.9C303.1 165.9 291.6 177.4 291.6 191.7000000000001C291.6 205.9 303 217.5000000000001 317.2 217.5000000000001C331.3 217.5000000000001 342.8 205.9000000000001 342.8 191.7000000000001zM164.9 191.7000000000001C164.9 177.5000000000001 153.5 165.9 139.3 165.9C125.2 165.9 113.7 177.4 113.7 191.7000000000001C113.7 205.9 125.1 217.5000000000001 139.3 217.5000000000001C153.5 217.5000000000001 164.9 205.9000000000001 164.9 191.7000000000001z" />

Before

Width:  |  Height:  |  Size: 608 KiB

After

Width:  |  Height:  |  Size: 609 KiB

View File

@@ -236,7 +236,7 @@
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM312 40C312 4.4 282.9 -24.5 247.1 -24C212 -23.5 184 5.8 184 41V83.8L201.7 92.6C216.7 100.1 233.2 90.9 236.6 76.1L239.4 64C241.5 54.8 254.6 54.8 256.7 64L259.5 76.1C262.9 90.9 279.3 100.2000000000001 294.4 92.6L312.1 83.8V40zM340.2 14.7C342.4 22.8 344 31.2 344 40V83.5C358.2 95.9 368.4 111 371.3 128C373 137.9 363.6 146.5 353.6 143.3C327.7000000000001 135 289.2000000000001 130.2 248.0000000000001 130.2S168.4 135 142.4 143.3C132.5 146.4 123 138 124.7 128C127.6 111 137.8000000000001 95.9 152 83.5V40C152 31.2 153.6 22.8 155.8000000000001 14.7C91.8 48.1 48 115 48 192C48 302.3 137.7 392 248 392S448 302.3 448 192C448 115 404.2 48.1 340.2 14.7zM168 272C150.3 272 136 257.7 136 240S150.3 208 168 208S200 222.3 200 240S185.7 272 168 272zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272z" />
<glyph glyph-name="grin-wink"
unicode="&#xF58C;"
horiz-adv-x="496" d=" M0 192C0 55 111 -56 248 -56S496 55 496 192S385 440 248 440S0 329 0 192zM200 240C200 222.3 185.7 208 168 208S136 222.3 136 240S150.3 272 168 272S200 257.7 200 240zM368 215L358.5 223.5C343.7 236.7 312.3 236.7 297.5 223.5L288 215C279.7 207.6 266.4 214.6 268.2 225.8C272.2 251 302.4 267.9 328.1 267.9S384 251 388 225.8C389.6 214.8 376.5 207.6 368 215zM124.9 127.2C155.1 117.5 200 112 248 112S340.9 117.5 371.1 127.2C382.4000000000001 130.9 393.7000000000001 121.2 391.8 109.3C382.6 54.3 308.6 16 248 16S113.5 54.3 104.2 109.3C102.2 121.2 113.5 130.9 124.9 127.2z" />
horiz-adv-x="496" d=" M328 268C302.31 268 272.12 251.08 268.14 225.88C266.39 214.66 279.64 207.64 287.97 215.04L297.52 223.52C312.33 236.71 343.68 236.71 358.49 223.52L368.04 215.04C376.5200000000001 207.61 389.6 214.79 387.87 225.88C383.88 251.08 353.69 268 328 268zM168 208C185.67 208 200 222.33 200 240S185.67 272 168 272S136 257.67 136 240S150.33 208 168 208zM353.55 143.36C327.62 135.06 289.15 130.3 248 130.3S168.38 135.05 142.45 143.36C132.51 146.49 123.05 137.99 124.74 128.0200000000001C132.67 80.87 196.06 48 248 48S363.33 80.87 371.26 128.02C372.94 137.91 363.59 146.5 353.55 143.36zM248 440C111.03 440 0 328.9700000000001 0 192S111.03 -56 248 -56S496 55.03 496 192S384.9700000000001 440 248 440zM248 -8C137.72 -8 48 81.72 48 192S137.72 392 248 392S448 302.28 448 192S358.28 -8 248 -8z" />
<glyph glyph-name="grin"
unicode="&#xF580;"
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM353.6 143.4C327.7000000000001 135.1 289.2000000000001 130.3 248.0000000000001 130.3S168.4 135.1 142.4 143.4C132.5 146.5 123 138 124.7 128.1C132.6 80.9999999999999 196 48.1 248.0000000000001 48.1S363.3 80.9999999999999 371.3 128.1C372.9000000000001 137.9 363.6 146.5 353.6 143.4zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 208C345.7 208 360 222.3 360 240S345.7 272 328 272S296 257.7 296 240S310.3 208 328 208z" />
@@ -311,7 +311,7 @@
horiz-adv-x="504" d=" M304 139.5C304 158.7 275.2 181 232.5 183.5C228.7 183.9 225.1 181.1 224.3 177.3C223.4 173.5 225.4 169.6 229 168.1L245.9 160.9C258.9 155.4 266.7 147.4 266.7 139.4S258.9 123.4 246 117.9L229 110.7000000000001C223.3 108.3000000000001 223 98.5000000000001 229 95.9L245.9 88.7000000000001C258.9 83.2000000000001 266.7 75.2000000000001 266.7 67.2000000000001S258.9 51.2 246 45.7L229 38.5000000000001C225.4 37.0000000000001 223.4 33.1000000000001 224.3 29.3000000000001C225.1 25.7 228.4 23.1000000000001 232.1 23.1000000000001H232.6C275.4000000000001 25.6000000000001 304.1 47.9000000000001 304.1 67.1000000000001C304.1 80.1000000000001 290.7000000000001 94.4000000000001 268.9000000000001 103.1000000000001C290.6 112.2000000000001 304.0000000000001 126.5000000000001 304.0000000000001 139.5000000000001zM374.5 223L384 214.5C387.8 211.2 393.3 210.5 397.7 212.9C402.1 215.3 404.6 220.3 403.8 225.3C399.8 250.5 369.6 267.4 344 267.4S288.1 250.5 284.2 225.3C283.4 220.3 285.9 215.3 290.3 212.9C296.1 209.8 301.5 212.2 304 214.5L313.5 223C328.3 236.2 359.7 236.2 374.5 223zM136 239.5C136 221.8 150.3 207.5 168 207.5S200 221.8 200 239.5S185.7 271.5 168 271.5S136 257.2 136 239.5zM501.1 45.5C493.1 66.3 469.6 77 448 71.4L439.6 69.2L437.3 77.6C431.4000000000001 98.9999999999999 410.3 114.1 388.3 110.6C363.1 106.6 347.7 81.9999999999999 354.3 57.9999999999999L377.2 -24.6C378.7 -29.9 384.2 -33.1 389.6 -31.7L472.6 -10.2C496.7 -3.9 510.3 21.6 501.1 45.4999999999999zM334 11.7C307.9 -0.8 278.8 -8 248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192C448 169.9 444.3 148.7 437.6 128.8C446.6 122.4 454.6 114.6 460.2 104.9C466.6 104.8 472.8000000000001 103.5000000000001 478.8000000000001 102.0000000000001C489.7 129.9 495.9000000000001 160.2000000000001 495.9000000000001 192.0000000000001C496 329 385 440 248 440S0 329 0 192S111 -56 248 -56C283.4 -56 316.9 -48.5 347.4 -35.1C344.9 -27.8 351.7 -52.3 334 11.7z" />
<glyph glyph-name="kiss"
unicode="&#xF596;"
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -24C128.9 -24 32 72.9 32 192S128.9 408 248 408S464 311.1 464 192S367.1 -24 248 -24zM304 140C304 159.2 275.2 181.5 232.5 184C228.7 184.4 225.1 181.6 224.3 177.8C223.4 174 225.4 170.1 229 168.6L245.9 161.4C258.9 155.9 266.7 147.9 266.7 139.9S258.9 123.9 246 118.4L229 111.2000000000001C223.3 108.8000000000001 223 99.0000000000001 229 96.4L245.9 89.2000000000001C258.9 83.7000000000001 266.7 75.7000000000001 266.7 67.7000000000001S258.9 51.7 246 46.2L229 39.0000000000001C225.4 37.5000000000001 223.4 33.6000000000001 224.3 29.8000000000001C225.1 26.2 228.4 23.6000000000001 232.1 23.6000000000001H232.6C275.4000000000001 26.1000000000001 304.1 48.4000000000001 304.1 67.6000000000001C304.1 80.6000000000001 290.7000000000001 94.9000000000001 268.9000000000001 103.6000000000001C290.6 112.7 304 127 304 140zM168 272C150.3 272 136 257.7 136 240S150.3 208 168 208S200 222.3 200 240S185.7 272 168 272zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272z" />
horiz-adv-x="496" d=" M168 272C150.3 272 136 257.7 136 240S150.3 208 168 208S200 222.3 200 240S185.7 272 168 272zM304 140C304 159.2 275.2 181.5 232.5 184C228.7 184.4 225.1 181.6 224.3 177.8C223.4 174 225.4 170.1 229 168.6L245.9 161.4C258.9 155.9 266.7 147.9 266.7 139.9S258.9 123.9 246 118.4L229 111.2000000000001C223.3 108.8000000000001 223 99.0000000000001 229 96.4L245.9 89.2000000000001C258.9 83.7000000000001 266.7 75.7000000000001 266.7 67.7000000000001S258.9 51.7 246 46.2L229 39.0000000000001C225.4 37.5000000000001 223.4 33.6000000000001 224.3 29.8000000000001C225.1 26.2 228.4 23.6000000000001 232.1 23.6000000000001H232.6C275.4000000000001 26.1000000000001 304.1 48.4000000000001 304.1 67.6000000000001C304.1 80.6000000000001 290.7000000000001 94.9000000000001 268.9000000000001 103.6000000000001C290.6 112.7 304 127 304 140zM248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272z" />
<glyph glyph-name="laugh-beam"
unicode="&#xF59A;"
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM389.4 50.6C351.6 12.8 301.4 -8 248 -8S144.4 12.8 106.6 50.6S48 138.6 48 192S68.8 295.6 106.6 333.4S194.6 392 248 392S351.6 371.2 389.4 333.4S448 245.4 448 192S427.2 88.4 389.4 50.6zM328 296C304.2 296 275.3 266.7 272 224.6C271.3 216 282.8 212.7 286.9 220.1L296.4 237.1C304.1 250.8 315.6 258.7 327.9 258.7S351.7 250.8 359.4 237.1L368.9 220.1C373 212.7 384.5 216.1 383.8 224.6C380.7 266.7 351.8 296 328 296zM127 220.1L136.5 237.1C144.2 250.8 155.7 258.7 168 258.7S191.8 250.8 199.5 237.1L209 220.1C213.1 212.7 224.6 216.1 223.9 224.6C220.6 266.7 191.7 296 167.9 296S115.2 266.7 111.9 224.6C111.3 216.1 122.8 212.7 127 220.1zM362.4 160H133.6C125.4 160 119.1 153 120.1 145C127.6 85.8 179 40 241.2 40H254.8C317 40 368.4 85.8 375.9 145C376.9 153 370.6 160 362.4 160z" />

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -1,28 +1,28 @@
{
"build/app.js": "/build/app.js?ab8e6e2c499b2137f638",
"build/app.css": "/build/app.css?2cd028b4cb27a6be36dc5e854ea1ada9",
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?128d2b0b",
"build/app.js": "/build/app.js?df75851c817558ca8803",
"build/app.css": "/build/app.css?b1944e8d2023603075ed8d913c21a8e8",
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
"build/images/boxed-bg.jpg": "/build/images/boxed-bg.jpg?7799dece",
"build/images/blue.png": "/build/images/blue.png?96f8a905",
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?18d2347a",
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
"build/images/fa-solid-900.svg": "/build/images/fa-solid-900.svg?666a82cb",
"build/images/glyphicons-halflings-regular.svg": "/build/images/glyphicons-halflings-regular.svg?89889688",
"build/fonts/glyphicons-halflings-regular.eot": "/build/fonts/glyphicons-halflings-regular.eot?f4769f9b",
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.ttf?00ddaede",
"build/fonts/fa-brands-400.eot": "/build/fonts/fa-brands-400.eot?e2a76403",
"build/fonts/fa-regular-400.eot": "/build/fonts/fa-regular-400.eot?5a4618f0",
"build/fonts/fa-brands-400.woff": "/build/fonts/fa-brands-400.woff?d034c1b2",
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.ttf?a7a790d4",
"build/fonts/fa-brands-400.eot": "/build/fonts/fa-brands-400.eot?69b310ed",
"build/fonts/fa-regular-400.eot": "/build/fonts/fa-regular-400.eot?0b7928f6",
"build/fonts/fa-brands-400.woff": "/build/fonts/fa-brands-400.woff?e9f83339",
"build/fonts/glyphicons-halflings-regular.woff2": "/build/fonts/glyphicons-halflings-regular.woff2?448c34a5",
"build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.eot?d9824d00",
"build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.eot?0b93480e",
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?e99569d3",
"build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.woff2?a3715c6f",
"build/images/fa-regular-400.svg": "/build/images/fa-regular-400.svg?e9d8dbb0",
"build/fonts/fa-regular-400.ttf": "/build/fonts/fa-regular-400.ttf?6534c603",
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.ttf?e0fc4e5c",
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124",
"build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.woff2?930c1264",
"build/images/fa-regular-400.svg": "/build/images/fa-regular-400.svg?4e41caaf",
"build/fonts/fa-regular-400.ttf": "/build/fonts/fa-regular-400.ttf?cdd8fa76",
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.ttf?d94572dd",
"build/fonts/glyphicons-halflings-regular.woff": "/build/fonts/glyphicons-halflings-regular.woff?fa277232",
"build/fonts/fa-brands-400.woff2": "/build/fonts/fa-brands-400.woff2?f319eac1",
"build/fonts/fa-brands-400.woff2": "/build/fonts/fa-brands-400.woff2?66f625f1",
"build/fonts/glyphicons-halflings-regular.ttf": "/build/fonts/glyphicons-halflings-regular.ttf?e18bbf61",
"build/images/fa-brands-400.svg": "/build/images/fa-brands-400.svg?a999c7b8",
"build/images/fa-brands-400.svg": "/build/images/fa-brands-400.svg?5da7e33c",
"build/images/default_avatar.png": "/build/images/default_avatar.png?fc197350"
}

View File

@@ -104,6 +104,7 @@ class CreateUserCommand extends Command
$user->setUsername($username)
->setPlainPassword($password)
->setEmail($email)
->setEnabled(true)
->setRoles(explode(',', $role))
;

View File

@@ -459,7 +459,7 @@ class KimaiImporterCommand extends Command
->setAlias($oldUser['alias'])
->setEmail($oldUser['mail'])
->setPlainPassword($password)
->setActive($isActive)
->setEnabled($isActive)
->setRoles([$role])
;

View File

@@ -72,6 +72,7 @@ class UserController extends AbstractController
$password = $this->get('security.password_encoder')
->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$user->setEnabled(true);
$user->setRoles([User::DEFAULT_ROLE]);
$entityManager = $this->getDoctrine()->getManager();

View File

@@ -1,46 +0,0 @@
<?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\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as SymfonyAbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
/**
* Controller used to manage the application security.
* See http://symfony.com/doc/current/cookbook/security/form_login_setup.html.
*/
class SecurityController extends SymfonyAbstractController
{
/**
* @Route("/login", name="security_login")
*/
public function login(AuthenticationUtils $helper): Response
{
return $this->render('security/login.html.twig', [
'last_username' => $helper->getLastUsername(),
'error' => $helper->getLastAuthenticationError(),
]);
}
/**
* This is the route the user can use to logout.
*
* But, this will never be executed. Symfony will intercept this first
* and handle the logout automatically. See logout in config/packages/security.yaml
*
* @Route("/logout", name="security_logout")
*/
public function logout(): void
{
throw new \Exception('This should never be reached!');
}
}

View File

@@ -70,7 +70,7 @@ class AppFixtures extends Fixture
->setEmail($userData[3])
->setRoles([$userData[4]])
->setAvatar($userData[5])
->setActive($userData[6])
->setEnabled($userData[6])
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
;

View File

@@ -0,0 +1,90 @@
<?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\Doctrine;
use Doctrine\Migrations\AbstractMigration as BaseAbstractMigration;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for all Doctrine migrations.
*/
abstract class AbstractMigration extends BaseAbstractMigration implements ContainerAwareInterface
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @param ContainerInterface $container
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
/**
* @return ContainerInterface
*/
public function getContainer()
{
return $this->container;
}
/**
* @param string $name
* @return string
*/
protected function getTableName($name)
{
return getenv('DATABASE_PREFIX') . $name;
}
/**
* @return string
* @throws \Doctrine\DBAL\DBALException
*/
protected function getPlatform()
{
return $this->connection->getDatabasePlatform()->getName();
}
/**
* Call me like this:
* $schema = $this->getClassMetaData(User::class);
*
* @param string $entityName
* @return \Doctrine\Common\Persistence\Mapping\ClassMetadata
*/
protected function getClassMetaData($entityName)
{
$em = $this->getContainer()->get('doctrine')->getManager();
return $em->getClassMetadata($entityName);
}
/**
* we do it via addSql instead of $schema->getTable($users)->dropIndex()
* otherwise the commands will be executed as last ones.
*
* @param string $indexName
* @param string $tableName
* @throws \Doctrine\DBAL\DBALException
*/
protected function addSqlDropIndex($indexName, $tableName)
{
$dropSql = 'DROP INDEX ' . $indexName;
if ($this->getPlatform() === 'mysql') {
$dropSql .= ' ON ' . $tableName;
}
$this->addSql($dropSql);
}
}

View File

@@ -9,10 +9,10 @@
namespace App\Entity;
use App\Validator\Constraints as KimaiAssert;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
@@ -24,14 +24,14 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\Table(
* name="users",
* uniqueConstraints={
* @ORM\UniqueConstraint(columns={"name"}),
* @ORM\UniqueConstraint(columns={"mail"})
* @ORM\UniqueConstraint(columns={"username"}),
* @ORM\UniqueConstraint(columns={"email"})
* }
* )
* @UniqueEntity("username")
* @UniqueEntity("email")
*/
class User implements UserInterface
class User extends BaseUser implements UserInterface
{
public const ROLE_CUSTOMER = 'ROLE_CUSTOMER';
public const ROLE_USER = 'ROLE_USER';
@@ -47,40 +47,7 @@ class User implements UserInterface
* @ORM\GeneratedValue
* @ORM\Column(name="id", type="integer")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=60, nullable=false, unique=true)
* @Assert\NotBlank()
* @Assert\Length(min=5, max=60)
*/
private $username;
/**
* @var string
*
* @ORM\Column(name="mail", type="string", length=160, nullable=false, unique=true)
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;
/**
* @var string
*
* @ORM\Column(name="password", type="string", length=254, nullable=true)
*/
private $password;
/**
* @var string
*
* @Assert\NotBlank(groups={"registration", "passwordUpdate"})
* @Assert\Length(min=6, max=4096, groups={"registration", "passwordUpdate"})
*/
private $plainPassword;
protected $id;
/**
* @var string
@@ -90,14 +57,6 @@ class User implements UserInterface
*/
private $alias;
/**
* @var bool
*
* @ORM\Column(name="active", type="boolean", nullable=false)
* @Assert\NotNull()
*/
private $active = true;
/**
* @var \DateTime
*
@@ -119,14 +78,6 @@ class User implements UserInterface
*/
private $avatar;
/**
* @var string[]
*
* @ORM\Column(type="json_array")
* @KimaiAssert\Role()
*/
private $roles = [];
/**
* @var UserPreference[]|Collection
*
@@ -139,6 +90,7 @@ class User implements UserInterface
*/
public function __construct()
{
parent::__construct();
$this->registeredAt = new \DateTime();
$this->preferences = new ArrayCollection();
}
@@ -189,107 +141,6 @@ class User implements UserInterface
return $this->alias;
}
/**
* @param bool $active
* @return $this
*/
public function setActive($active)
{
$this->active = (bool) $active;
return $this;
}
/**
* @return bool
*/
public function isActive()
{
return $this->active;
}
/**
* @param string $password
* @return $this
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Get password
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Only for form editing, you don't need this method!
*
* @return string
*/
public function getPlainPassword()
{
return $this->plainPassword;
}
/**
* Only for form editing, you don't need this method!
*
* @param string $password
* @return $this
*/
public function setPlainPassword($password)
{
$this->plainPassword = $password;
return $this;
}
/**
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* @param string $username
* @return $this
*/
public function setUsername($username)
{
$this->username = $username;
return $this;
}
/**
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* @param string $email
* @return $this
*/
public function setEmail($email)
{
$this->email = $email;
return $this;
}
/**
* @return string
*/
@@ -328,34 +179,6 @@ class User implements UserInterface
return $this;
}
/**
* Returns the roles or permissions granted to the user for security.
*
* @return string[]
*/
public function getRoles()
{
$roles = $this->roles;
// guarantees that a user always has at least one role for security
if (empty($roles)) {
$roles[] = 'ROLE_USER';
}
return array_unique($roles);
}
/**
* @param string[] $roles
* @return $this
*/
public function setRoles(array $roles)
{
$this->roles = $roles;
return $this;
}
/**
* @return UserPreference[]|Collection
*/
@@ -419,35 +242,6 @@ class User implements UserInterface
return $this;
}
/**
* @return bool
*/
public function isEnabled()
{
return $this->isActive();
}
/**
* Returns the salt that was originally used to encode the password.
*/
public function getSalt()
{
// See "Do you need to use a Salt?" at http://symfony.com/doc/current/cookbook/security/entity_provider.html
// we're using bcrypt in security.yml to encode the password, so
// the salt value is built-in and you don't have to generate one
return;
}
/**
* Removes sensitive data from the user.
*/
public function eraseCredentials()
{
// if you had a plainPassword property, you'd nullify it here
// $this->plainPassword = null;
}
/**
* @return string
*/

View File

@@ -132,7 +132,7 @@ class UserPreference
}
/**
* @return string|int|bool
* @return mixed
*/
public function getValue()
{
@@ -147,7 +147,10 @@ class UserPreference
}
/**
* @param string|int|bool $value
* Given $value will not be serialized before its stored, so it should be one of the types:
* integer, string or boolean
*
* @param mixed $value
* @return UserPreference
*/
public function setValue($value): UserPreference

View File

@@ -12,6 +12,7 @@ namespace App\EventSubscriber;
use App\Event\ConfigureAdminMenuEvent;
use App\Event\ConfigureMainMenuEvent;
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
use KevinPapst\AdminLTEBundle\Event\ThemeEvents;
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -48,7 +49,7 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents(): array
{
return [
'theme.sidebar_setup_menu' => ['onSetupNavbar', 100],
ThemeEvents::THEME_SIDEBAR_SETUP_MENU => ['onSetupNavbar', 100],
];
}
@@ -89,7 +90,7 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
}
$event->addItem(
new MenuItemModel('logout', 'menu.logout', 'security_logout', [], 'fas fa-sign-out-alt')
new MenuItemModel('logout', 'menu.logout', 'fos_user_security_logout', [], 'fas fa-sign-out-alt')
);
$this->activateByRoute(

View File

@@ -11,6 +11,7 @@ namespace App\EventSubscriber;
use App\Entity\User;
use KevinPapst\AdminLTEBundle\Event\ShowUserEvent;
use KevinPapst\AdminLTEBundle\Event\ThemeEvents;
use KevinPapst\AdminLTEBundle\Model\UserModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@@ -40,8 +41,8 @@ class NavbarShowUserSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents(): array
{
return [
'theme.navbar_user' => ['onShowUser', 100],
'theme.sidebar_user' => ['onShowUser', 100],
ThemeEvents::THEME_NAVBAR_USER => ['onShowUser', 100],
ThemeEvents::THEME_SIDEBAR_USER => ['onShowUser', 100],
];
}

View File

@@ -0,0 +1,62 @@
<?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\EventSubscriber;
use App\Entity\User;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* This class intercepts the registration to make sure the first-ever
* registered user will get the SUPER_ADMIN role.
*/
class RegistrationSubscriber implements EventSubscriberInterface
{
/**
* @var UserManagerInterface
*/
protected $userManager;
/**
* @param UserManagerInterface $userManager
*/
public function __construct(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
FOSUserEvents::REGISTRATION_SUCCESS => ['onRegistrationSuccess', 200]
];
}
/**
* @param FormEvent $event
*/
public function onRegistrationSuccess(FormEvent $event)
{
/** @var $user \FOS\UserBundle\Model\UserInterface */
$user = $event->getForm()->getData();
$roles = [User::ROLE_USER];
if (empty($this->userManager->findUsers())) {
$roles = [User::ROLE_SUPER_ADMIN];
}
$user->setRoles($roles);
}
}

View File

@@ -47,7 +47,7 @@ class UserEditType extends AbstractType
'label' => 'label.email',
])
// boolean
->add('active', YesNoType::class, [
->add('enabled', YesNoType::class, [
'label' => 'label.active',
])
;

View File

@@ -0,0 +1,106 @@
<?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;
/**
* Initial database structure of Kimai 2.
* This file is only required for testing the migrations!
*/
final class Version20180701120000 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);
}
if ($platform === 'sqlite') {
$this->addSql('CREATE TABLE ' . $this->getTableName('users') . ' (id INTEGER NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active BOOLEAN NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles CLOB NOT NULL --(DC2Type:json_array)
, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 ON ' . $this->getTableName('users') . ' (name)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 ON ' . $this->getTableName('users') . ' (mail)');
$this->addSql('CREATE TABLE ' . $this->getTableName('user_preferences') . ' (id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_8D08F631A76ED395 ON ' . $this->getTableName('user_preferences') . ' (user_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 ON ' . $this->getTableName('user_preferences') . ' (user_id, name)');
$this->addSql('CREATE TABLE ' . $this->getTableName('customers') . ' (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address CLOB DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE ' . $this->getTableName('projects') . ' (id INTEGER NOT NULL, customer_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $this->getTableName('projects') . ' (customer_id)');
$this->addSql('CREATE TABLE ' . $this->getTableName('activities') . ' (id INTEGER NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $this->getTableName('activities') . ' (project_id)');
$this->addSql('CREATE TABLE ' . $this->getTableName('timesheet') . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $this->getTableName('timesheet') . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $this->getTableName('timesheet') . ' (activity_id)');
$this->addSql('CREATE TABLE ' . $this->getTableName('invoice_templates') . ' (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address CLOB DEFAULT NULL, due_days INTEGER NOT NULL, vat INTEGER DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms CLOB DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_1626CFE95E237E06 ON ' . $this->getTableName('invoice_templates') . ' (name)');
} else {
$this->addSql('CREATE TABLE ' . $this->getTableName('users') . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles JSON NOT NULL COMMENT \'(DC2Type:json_array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $this->getTableName('user_preferences') . ' (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $this->getTableName('customers') . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $this->getTableName('projects') . ' (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $this->getTableName('activities') . ' (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $this->getTableName('timesheet') . ' (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $this->getTableName('invoice_templates') . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE ' . $this->getTableName('user_preferences') . ' ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES ' . $this->getTableName('users') . ' (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE ' . $this->getTableName('projects') . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $this->getTableName('customers') . ' (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE ' . $this->getTableName('activities') . ' ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $this->getTableName('projects') . ' (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE ' . $this->getTableName('timesheet') . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $this->getTableName('users') . ' (id)');
$this->addSql('ALTER TABLE ' . $this->getTableName('timesheet') . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $this->getTableName('activities') . ' (id) ON DELETE CASCADE');
}
}
/**
* @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);
}
if ($platform === 'sqlite') {
$this->addSql('DROP TABLE ' . $this->getTableName('users'));
$this->addSql('DROP TABLE ' . $this->getTableName('user_preferences'));
$this->addSql('DROP TABLE ' . $this->getTableName('customers'));
$this->addSql('DROP TABLE ' . $this->getTableName('projects'));
$this->addSql('DROP TABLE ' . $this->getTableName('activities'));
$this->addSql('DROP TABLE ' . $this->getTableName('timesheet'));
$this->addSql('DROP TABLE ' . $this->getTableName('invoice_templates'));
} else {
$this->addSql('ALTER TABLE ' . $this->getTableName('user_preferences') . ' DROP FOREIGN KEY FK_8D08F631A76ED395');
$this->addSql('ALTER TABLE ' . $this->getTableName('timesheet') . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
$this->addSql('ALTER TABLE ' . $this->getTableName('projects') . ' DROP FOREIGN KEY FK_407F12069395C3F3');
$this->addSql('ALTER TABLE ' . $this->getTableName('activities') . ' DROP FOREIGN KEY FK_8811FE1C166D1F9C');
$this->addSql('ALTER TABLE ' . $this->getTableName('timesheet') . ' DROP FOREIGN KEY FK_4F60C6B181C06096');
$this->addSql('DROP TABLE ' . $this->getTableName('users'));
$this->addSql('DROP TABLE ' . $this->getTableName('user_preferences'));
$this->addSql('DROP TABLE ' . $this->getTableName('customers'));
$this->addSql('DROP TABLE ' . $this->getTableName('projects'));
$this->addSql('DROP TABLE ' . $this->getTableName('activities'));
$this->addSql('DROP TABLE ' . $this->getTableName('timesheet'));
$this->addSql('DROP TABLE ' . $this->getTableName('invoice_templates'));
}
}
}

View File

@@ -0,0 +1,127 @@
<?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\Index;
use Doctrine\DBAL\Schema\Schema;
/**
* Migration for FOSUserBundle
*
* Changes the table structure of "users" table and migrates from json_array type to serialized array,
* probably also fixing the higher required MariaDB version.
*/
final class Version20180715160326 extends AbstractMigration
{
/**
* @var Index[]
*/
protected $indexesOld = [];
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\DBALException
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
* @throws \Doctrine\DBAL\Schema\SchemaException
*/
public function up(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$users = $this->getTableName('users');
// delete all existing indexes
$indexesOld = $schema->getTable($users)->getIndexes();
foreach ($indexesOld as $index) {
if (in_array('name', $index->getColumns()) || in_array('mail', $index->getColumns())) {
$this->indexesOld[] = $index;
$this->addSqlDropIndex($index->getName(), $users);
}
}
if ($platform === 'sqlite') {
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $users . ' AS SELECT id, name, mail, password, alias, active, registration_date, title, avatar, roles FROM ' . $users);
$this->addSql('DROP TABLE ' . $users);
$this->addSql('CREATE TABLE ' . $users . ' (id INTEGER NOT NULL, alias VARCHAR(60) DEFAULT NULL COLLATE BINARY, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL COLLATE BINARY, avatar VARCHAR(255) DEFAULT NULL COLLATE BINARY, enabled BOOLEAN NOT NULL, password VARCHAR(255) NOT NULL, roles CLOB NOT NULL --(DC2Type:array)
, username VARCHAR(180) NOT NULL, username_canonical VARCHAR(180) NOT NULL, email VARCHAR(180) NOT NULL, email_canonical VARCHAR(180) NOT NULL, salt VARCHAR(255) DEFAULT NULL, last_login DATETIME DEFAULT NULL, confirmation_token VARCHAR(180) DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO ' . $users . ' (id, username, username_canonical, email, email_canonical, password, alias, enabled, registration_date, title, avatar, roles) SELECT id, name, name, mail, mail, password, alias, active, registration_date, title, avatar, roles FROM __temp__' . $users);
$this->addSql('DROP TABLE __temp__' . $users);
} else {
$this->addSql('ALTER TABLE ' . $users . ' CHANGE name username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, CHANGE mail email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, CHANGE password password VARCHAR(255) NOT NULL, CHANGE alias alias VARCHAR(60) DEFAULT NULL, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE active enabled TINYINT(1) NOT NULL');
$this->addSql('UPDATE ' . $users . ' set username_canonical = username');
$this->addSql('UPDATE ' . $users . ' set email_canonical = email');
}
$this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:16:"ROLE_SUPER_ADMIN";}\' WHERE roles LIKE "%ROLE_SUPER_ADMIN%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:10:"ROLE_ADMIN";}\' WHERE roles LIKE "%ROLE_ADMIN%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:13:"ROLE_TEAMLEAD";}\' WHERE roles LIKE "%ROLE_TEAMLEAD%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'a:0:{}\' WHERE roles LIKE "%ROLE_USER%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:13:"ROLE_CUSTOMER";}\' WHERE roles LIKE "%ROLE_CUSTOMER%"');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON ' . $users . ' (username_canonical)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON ' . $users . ' (email_canonical)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEC05FB297 ON ' . $users . ' (confirmation_token)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEF85E0677 ON ' . $users . ' (username)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEE7927C74 ON ' . $users . ' (email)');
}
/**
* @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);
}
$users = $this->getTableName('users');
$indexToDelete = ['UNIQ_B9AC5BCE92FC23A8', 'UNIQ_B9AC5BCEA0D96FBF', 'UNIQ_B9AC5BCEC05FB297', 'UNIQ_B9AC5BCEF85E0677', 'UNIQ_B9AC5BCEE7927C74'];
foreach ($indexToDelete as $index) {
$this->addSqlDropIndex($index, $users);
}
if ($platform === 'sqlite') {
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $users . ' AS SELECT id, username, email, enabled, password, roles, alias, registration_date, title, avatar FROM ' . $users);
$this->addSql('DROP TABLE ' . $users);
$this->addSql('CREATE TABLE ' . $users . ' (id INTEGER NOT NULL, alias VARCHAR(60) DEFAULT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, active BOOLEAN NOT NULL, password VARCHAR(254) DEFAULT NULL COLLATE BINARY, roles CLOB NOT NULL COLLATE BINARY --(DC2Type:json_array)
, name VARCHAR(60) NOT NULL COLLATE BINARY, mail VARCHAR(160) NOT NULL COLLATE BINARY, PRIMARY KEY(id))');
$this->addSql('INSERT INTO ' . $users . ' (id, name, mail, active, password, roles, alias, registration_date, title, avatar) SELECT id, username, email, enabled, password, roles, alias, registration_date, title, avatar FROM __temp__' . $users);
$this->addSql('DROP TABLE __temp__' . $users);
} else {
$this->addSql('ALTER TABLE ' . $users . ' CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE roles roles JSON NOT NULL COLLATE utf8mb4_bin COMMENT \'(DC2Type:json_array)\', CHANGE alias alias VARCHAR(60) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT \'NULL\', CHANGE title title VARCHAR(50) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL');
}
$this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_SUPER_ADMIN"]\' WHERE roles LIKE "%ROLE_SUPER_ADMIN%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_ADMIN"]\' WHERE roles LIKE "%ROLE_ADMIN%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_TEAMLEAD"]\' WHERE roles LIKE "%ROLE_TEAMLEAD%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_USER"]\' WHERE roles LIKE "%ROLE_USER%"');
$this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_CUSTOMER"]\' WHERE roles LIKE "%ROLE_CUSTOMER%"');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 ON ' . $users . ' (name)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 ON ' . $users . ' (mail)');
$usersTable = $schema->getTable($users);
foreach ($this->indexesOld as $index) {
$usersTable->addIndex($index->getColumns(), $index->getName(), $index->getFlags(), $index->getOptions());
}
}
}

View File

@@ -60,13 +60,20 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
->orderBy('u.' . $query->getOrderBy(), $query->getOrder());
if (UserQuery::SHOW_VISIBLE == $query->getVisibility()) {
$qb->andWhere('u.active = 1');
$qb->andWhere('u.enabled = 1');
} elseif (UserQuery::SHOW_HIDDEN == $query->getVisibility()) {
$qb->andWhere('u.active = 0');
$qb->andWhere('u.enabled = 0');
}
if ($query->getRole() !== null) {
$qb->andWhere('u.roles LIKE :role')->setParameter('role', '%' . $query->getRole() . '%');
$rolesWhere = 'u.roles LIKE :role';
$qb->setParameter('role', '%' . $query->getRole() . '%');
// a hack as FOSUserBundle does not save the ROLE_USER in the database as it is the default role
if ($query->getRole() === User::ROLE_USER) {
$rolesWhere .= ' OR u.roles LIKE :role1';
$qb->setParameter('role1', '%{}');
}
$qb->andWhere($rolesWhere);
}
return $this->getPager($qb->getQuery(), $query->getPage(), $query->getPageSize());

View File

@@ -10,19 +10,28 @@
namespace App\Security;
use App\Entity\User;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Advanced checks during aithentication to make sure the user is allowed to use Kimai.
* Advanced checks during authentication to make sure the user is allowed to use Kimai.
*/
class UserChecker implements UserCheckerInterface
{
/**
* @param UserInterface $user
* @throws AccountStatusException
*/
public function checkPreAuth(UserInterface $user)
{
}
/**
* @param UserInterface $user
* @throws AccountStatusException
*/
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof User) {

View File

@@ -110,6 +110,9 @@
"ref": "bb31a3bbec00a8fc8aa1c9fbf9b0ef9fc492f93d"
}
},
"friendsofsymfony/user-bundle": {
"version": "v2.1.2"
},
"jdorn/sql-formatter": {
"version": "v1.2.17"
},
@@ -134,6 +137,9 @@
"pagerfanta/pagerfanta": {
"version": "v1.0.5"
},
"paragonie/random_compat": {
"version": "v2.0.17"
},
"phar-io/manifest": {
"version": "1.0.1"
},
@@ -428,6 +434,9 @@
"ref": "8daf8ede72274989fb0dc66b83cb4bc1693afbb9"
}
},
"symfony/templating": {
"version": "v4.1.1"
},
"symfony/thanks": {
"version": "v1.0.7"
},

View File

@@ -38,7 +38,7 @@
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">{{ entry.username }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'active') }}">{{ widgets.label_visible(entry.active) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'active') }}">{{ widgets.label_visible(entry.enabled) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'roles') }}">
{% for role in entry.roles %}
{{ widgets.label_role(role) }}

View File

@@ -67,11 +67,11 @@
{% endif %}
{% endblock %}
{% block avanzu_navbar_tasks %}{#{ parent() }#}{% endblock %}
{% block avanzu_navbar_user %}{#{ parent() }#}{% endblock %}
{% block avanzu_sidebar_user %}{# right now we simply deactivate that box #}{% endblock %}
{% block avanzu_sidebar_search %}{# right now we simply deactivate that box #}{% endblock %}
{# deactivated blocks #}
{% block avanzu_navbar_tasks %}{% endblock %}
{% block avanzu_navbar_user %}{% endblock %}
{% block avanzu_sidebar_user %}{% endblock %}
{% block avanzu_sidebar_search %}{% endblock %}
{% block avanzu_breadcrumb %}
{{ block('page_actions') }}

View File

@@ -0,0 +1,16 @@
{% extends '@AdminLTE/FOSUserBundle/Registration/confirmed.html.twig' %}
{% block logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block head %}
{{ parent() }}
<script src="{{ asset('build/app.js') }}"></script>
{% include 'partials/head.html.twig' %}
{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}
{% block javascripts %}{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends '@AdminLTE/FOSUserBundle/Registration/register.html.twig' %}
{% block logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block head %}
{{ parent() }}
<script src="{{ asset('build/app.js') }}"></script>
{% include 'partials/head.html.twig' %}
{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}
{% block javascripts %}{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends '@AdminLTE/FOSUserBundle/Resetting/request.html.twig' %}
{% block logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block head %}
{{ parent() }}
<script src="{{ asset('build/app.js') }}"></script>
{% include 'partials/head.html.twig' %}
{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}
{% block javascripts %}{% endblock %}

View File

@@ -1,19 +1,13 @@
{% extends '@AdminLTE/layout/login-layout.html.twig' %}
{% extends '@AdminLTE/FOSUserBundle/Security/login.html.twig' %}
{% block login_social_auth %}{% endblock %}
{% block login_actions %}{% endblock %}
{% block logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}
{% block login_form_end %}
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/>
{% endblock %}
{% block head %}
{{ parent() }}
{% include 'partials/head.html.twig' %}
{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends '@AdminLTE/FOSUserBundle/layout.html.twig' %}
{% block logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block head %}
{{ parent() }}
<script src="{{ asset('build/app.js') }}"></script>
{% include 'partials/head.html.twig' %}
{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}
{% block javascripts %}{% endblock %}

15
tests/.env.dist.mysql Normal file
View File

@@ -0,0 +1,15 @@
DATABASE_PREFIX=abc_
MAILER_FROM=kimai@example.com
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=c88c14fa70a424e7a12d459b9dd9df7f
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://travis@127.0.0.1:3306/kimai_migrations
###< doctrine/doctrine-bundle ###
###> symfony/swiftmailer-bundle ###
MAILER_URL=null://localhost
###< symfony/swiftmailer-bundle ###

15
tests/.env.dist.sqlite Normal file
View File

@@ -0,0 +1,15 @@
DATABASE_PREFIX=abc_
MAILER_FROM=kimai@example.com
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=c88c14fa70a424e7a12d459b9dd9df7f
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai_migrations.sqlite
###< doctrine/doctrine-bundle ###
###> symfony/swiftmailer-bundle ###
MAILER_URL=null://localhost
###< symfony/swiftmailer-bundle ###

View File

@@ -15,6 +15,7 @@ use App\Tests\Controller\ControllerBaseTest;
/**
* @coversDefaultClass \App\Controller\Admin\ActivityController
* @group integration
* @group legacy
*/
class ActivityControllerTest extends ControllerBaseTest
{

View File

@@ -15,6 +15,7 @@ use App\Tests\Controller\ControllerBaseTest;
/**
* @coversDefaultClass \App\Controller\Admin\UserController
* @group integration
* @group legacy
*/
class UserControllerTest extends ControllerBaseTest
{
@@ -30,4 +31,62 @@ class UserControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/user/');
$this->assertHasDataTable($client);
}
/**
* @dataProvider getValidationTestData
*/
public function testValidationForCreateAction(array $formData, array $validationFields)
{
$this->assertFormHasValidationError(
User::ROLE_SUPER_ADMIN,
'/admin/user/create',
'form[name=user_create]',
$formData,
$validationFields
);
}
public function getValidationTestData()
{
return [
[
// invalid fields: username, password_second, email, enabled
[
'user_create' => [
'username' => '',
'plainPassword' => ['first' => 'sdfsdf'],
'alias' => 'ycvyxcb',
'title' => '34rtwrtewrt',
'avatar' => 'asdfawer',
'email' => '',
]
],
[
'#user_create_username',
'#user_create_plainPassword_first',
'#user_create_email',
]
],
// invalid fields: username, password, email, enabled
[
[
'user_create' => [
'username' => '',
'plainPassword' => ['first' => 'sdfsdf', 'second' => 'sdfxxx'],
'alias' => 'ycvyxcb',
'title' => '34rtwrtewrt',
'avatar' => 'asdfawer',
'email' => 'ydfbvsdfgs', // email is not working
'enabled' => '3',
]
],
[
'#user_create_username',
'#user_create_plainPassword_first',
'#user_create_email',
'#user_create_enabled',
]
],
];
}
}

View File

@@ -103,7 +103,6 @@ abstract class ControllerBaseTest extends WebTestCase
/**
* @param string $url
* @param string $method
* @param Client|null $client
*/
protected function assertUrlIsSecured(string $url, $method = 'GET')
{
@@ -163,4 +162,41 @@ abstract class ControllerBaseTest extends WebTestCase
{
$this->assertContains('<table class="table table-striped table-hover dataTable" role="grid">', $client->getResponse()->getContent());
}
/**
* @param string $role the USER role to use for the request
* @param string $url the URL of the page displaying the initial form to submit
* @param string $formSelector a selector to find the form to test
* @param array $formData values to fill in the form
* @param array $fieldNames array of form-fields that should fail
* @param bool $disableValidation whether the form should validate before submitting or not
*/
protected function assertFormHasValidationError($role, $url, $formSelector, array $formData, array $fieldNames, $disableValidation = true)
{
$client = $this->getClientForAuthenticatedUser($role);
$crawler = $client->request('GET', '/' . self::DEFAULT_LANGUAGE . $url);
$form = $crawler->filter($formSelector)->form();
if ($disableValidation) {
$form->disableValidation();
}
$result = $client->submit($form, $formData);
$submittedForm = $result->filter($formSelector);
$validationErrors = $submittedForm->filter('li.text-danger');
$this->assertEquals(
count($fieldNames),
count($validationErrors),
sprintf('Expected %s validation errors, found %s', count($fieldNames), count($validationErrors))
);
foreach ($fieldNames as $name) {
$field = $submittedForm->filter($name);
$this->assertNotNull($field, 'Could not find form field: ' . $name);
$list = $field->nextAll();
$this->assertNotNull($list, 'Form field has no validation message: ' . $name);
$validation = $list->filter('li.text-danger');
$this->assertGreaterThanOrEqual(1, count($validation), 'Form field has no validation message: ' . $name);
}
}
}

View File

@@ -9,49 +9,61 @@
namespace App\Tests\Entity;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validation;
/**
* @covers \App\Entity\Timesheet
*/
abstract class AbstractEntityTest extends TestCase
abstract class AbstractEntityTest extends KernelTestCase
{
/**
* @param $value
* @param $entity
* @param array|string $fieldNames
*/
protected function assertHasViolationForField($value, $fieldNames)
protected function assertHasViolationForField($entity, $fieldNames)
{
$validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator();
$validations = $validator->validate($value);
self::bootKernel();
$validator = static::$kernel->getContainer()->get('validator');
$violations = $validator->validate($entity);
if (!is_array($fieldNames)) {
$fieldNames = [$fieldNames];
}
$expected = count($fieldNames);
$actual = $violations->count();
$this->assertEquals($expected, $actual, sprintf('Expected %s violations, found %s.', $expected, $actual));
$violatedFields = [];
/** @var ConstraintViolationInterface $validation */
foreach ($validations as $validation) {
$violatedFields[] = $validation->getPropertyPath();
foreach ($violations as $validation) {
$violatedFields[$validation->getPropertyPath()] = $validation->getPropertyPath();
}
foreach ($fieldNames as $id => $propertyPath) {
$foundField = false;
if (in_array($propertyPath, $violatedFields)) {
$foundField = true;
unset($violatedFields[$id]);
unset($violatedFields[$propertyPath]);
}
$this->assertTrue($foundField, 'Failed finding violation for field: ' . $propertyPath);
}
$this->assertEmpty($violatedFields, sprintf('Unexpected violations found: %s', implode(', ', $violatedFields)));
}
$expected = count($fieldNames);
$actual = $validations->count();
protected function assertHasNoViolations($entity)
{
self::bootKernel();
$validator = static::$kernel->getContainer()->get('validator');
$this->assertEquals($expected, $actual, sprintf('Expected %s violations, found %s.', $expected, $actual));
$violations = $validator->validate($entity);
$actual = $violations->count();
$this->assertEquals(0, $actual, sprintf('Expected 0 violations, found %s.', $actual));
}
}

110
tests/Entity/UserTest.php Normal file
View File

@@ -0,0 +1,110 @@
<?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\Entity;
use App\Entity\User;
use App\Entity\UserPreference;
/**
* @covers \App\Entity\User
*/
class UserTest extends AbstractEntityTest
{
public function getInvalidTestData()
{
return [
['', ''],
[null, null],
['test', 'test@'], // too short username
[str_pad('#', 61, '-'), 'test@x.'], // too long username
[str_pad('#', 61, '-'), 'test@x.', ['xxxxx']], // too short password and invalid role
];
}
/**
* @dataProvider getInvalidTestData
*/
public function testInvalidValues($username, $email, $roles = [])
{
$defaultFields = [
'username', 'email'
];
$user = new User();
$user->setUsername($username);
$user->setEmail($email);
if (!empty($roles)) {
$user->setRoles($roles);
$defaultFields[] = 'roles';
}
$this->assertHasViolationForField($user, $defaultFields);
}
public function getValidTestData()
{
return [
[str_pad('#', 6, '-'), 'test@x.x'], // shortest possible username
[str_pad('#', 60, '-'), 'test@x.x', ['ROLE_CUSTOMER']], // longest possible password and valid role
];
}
/**
* @dataProvider getValidTestData
*/
public function testValidValues($username, $email, $roles = [])
{
$user = new User();
$user->setUsername($username);
$user->setEmail($email);
if (!empty($roles)) {
$user->setRoles($roles);
}
$this->assertHasNoViolations($user);
}
public function testDatetime()
{
$date = new \DateTime('+1 day');
$user = new User();
$user->setRegisteredAt($date);
$this->assertEquals($date, $user->getRegisteredAt());
}
public function testPreferences()
{
$user = new User();
$this->assertNull($user->getPreference('test'));
$this->assertNull($user->getPreferenceValue('test'));
$this->assertEquals('foo', $user->getPreferenceValue('test', 'foo'));
$preference = new UserPreference();
$preference
->setName('test')
->setValue('foobar');
$user->addPreference($preference);
$this->assertEquals('foobar', $user->getPreferenceValue('test', 'foo'));
$this->assertEquals($preference, $user->getPreference('test'));
}
public function testToString()
{
$user = new User();
$user->setUsername('bar');
$this->assertEquals('bar', (string) $user);
$this->assertEquals('bar', $user->getUsername());
$user->setAlias('foo');
$this->assertEquals('foo', (string) $user);
$this->assertEquals('foo', $user->getAlias());
}
}

View File

@@ -0,0 +1,68 @@
<?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\Voter;
use App\Entity\User;
use App\EventSubscriber\RegistrationSubscriber;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserManagerInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* @covers \App\EventSubscriber\RegistrationSubscriber
*/
class RegistrationSubscriberTest extends TestCase
{
public function testGetSubscribedEvents()
{
$events = RegistrationSubscriber::getSubscribedEvents();
$this->assertArrayHasKey(FOSUserEvents::REGISTRATION_SUCCESS, $events);
$methodName = $events[FOSUserEvents::REGISTRATION_SUCCESS][0];
$this->assertTrue(method_exists(RegistrationSubscriber::class, $methodName));
}
/**
* @dataProvider getTestData
*/
public function testRoleAssignmentForNewUser(array $existingUsers, $expectedRoles)
{
$user = new User();
$user->setAlias('foo');
$this->assertEquals([User::ROLE_USER], $user->getRoles());
$userManager = $this->getMockBuilder(UserManagerInterface::class)->getMock();
$userManager->method('findUsers')->willReturn($existingUsers);
$form = $this->getMockBuilder(FormInterface::class)->getMock();
$form->method('getData')->willReturn($user);
$request = $this->getMockBuilder(Request::class)->getMock();
$event = new FormEvent($form, $request);
$sut = new RegistrationSubscriber($userManager);
$sut->onRegistrationSuccess($event);
$this->assertEquals($expectedRoles, $user->getRoles());
}
public function getTestData()
{
return [
// NewFirstUserGetsSuperAdminRole
[[], [User::ROLE_SUPER_ADMIN, User::ROLE_USER]],
// NewUserGetUserRole
[[new User()], [User::ROLE_USER]],
];
}
}

View File

@@ -82,7 +82,7 @@ class TimesheetQueryTest extends BaseQueryTest
{
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState());
$sut->setState('foo-bar');
$sut->setState(PHP_INT_MAX);
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState());
$sut->setState(TimesheetQuery::STATE_STOPPED);

View File

@@ -0,0 +1,59 @@
<?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\Validator\Constraints;
use App\Entity\User;
use App\Validator\Constraints\Role;
use App\Validator\Constraints\RoleValidator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @covers \App\Validator\Constraints\RoleValidator
*/
class RoleValidatorTest extends TestCase
{
public function getValidRoles()
{
return [
[User::ROLE_CUSTOMER],
[User::ROLE_USER],
[User::ROLE_TEAMLEAD],
[User::ROLE_ADMIN],
[User::ROLE_SUPER_ADMIN],
];
}
/**
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException
*/
public function testConstraintIsInvalid()
{
$validator = new RoleValidator();
$validator->validate('foo', new NotBlank());
}
/**
* @dataProvider getValidRoles
*/
public function testConstraintWithValidRole($role)
{
$constraint = new Role();
$validator = new RoleValidator();
$validator->validate($role, $constraint);
// the above line would break if the role is invalid, we need the next assert to mark the test as valid
$this->assertNull(null);
}
public function testValidationError()
{
$this->markTestIncomplete(__CLASS__ . ': validation message not tested yet');
}
}

View File

@@ -40,15 +40,15 @@ class TimesheetVoterTest extends TestCase
public function getTestData()
{
$user0 = $this->getUser(0, 'ROLE_CUSTOMER');
$user1 = $this->getUser(1, 'ROLE_USER');
$user2 = $this->getUser(1, 'ROLE_TEAMLEAD');
$user0 = $this->getUser(0, User::ROLE_CUSTOMER);
$user1 = $this->getUser(1, User::ROLE_USER);
$user2 = $this->getUser(1, User::ROLE_TEAMLEAD);
return [
[$user0, false, new Customer(), ['edit'], VoterInterface::ACCESS_ABSTAIN],
[$user1, false, $this->getTimesheet($user1), ['edit'], VoterInterface::ACCESS_GRANTED],
[$user1, false, $this->getTimesheet($user0), ['edit'], VoterInterface::ACCESS_DENIED],
[$user2, true, $this->getTimesheet($user1), ['edit'], VoterInterface::ACCESS_GRANTED],
[$user0, false, new Customer(), [TimesheetVoter::EDIT], VoterInterface::ACCESS_ABSTAIN],
[$user1, false, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
[$user1, false, $this->getTimesheet($user0), [TimesheetVoter::EDIT], VoterInterface::ACCESS_DENIED],
[$user2, true, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
];
}

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="AdminLTEBundle.en.xliff">
<body>
<trans-unit id="Toggle navigation">
<source>Toggle navigation</source>
<target>Ansicht der Seitenleiste umschalten</target>
</trans-unit>
<trans-unit id="Remember Me">
<source>Remember Me</source>
<target>Dauerhaft anmelden</target>
</trans-unit>
<trans-unit id="Sign in to start your session">
<source>Sign in to start your session</source>
<target>Anmelden</target>
</trans-unit>
<trans-unit id="Sign In">
<source>Sign In</source>
<target>Anmelden</target>
</trans-unit>
<trans-unit id="Username">
<source>Username</source>
<target>Benutzername</target>
</trans-unit>
<trans-unit id="Password">
<source>Password</source>
<target>Passwort</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="en" datatype="plaintext" original="none">
<body>
<trans-unit id="Toggle navigation">
<source>Toggle navigation</source>
<target>Toggle navigation</target>
</trans-unit>
<trans-unit id="Remember Me">
<source>Remember Me</source>
<target>Remember Me</target>
</trans-unit>
<trans-unit id="Sign in to start your session">
<source>Sign in to start your session</source>
<target>Login</target>
</trans-unit>
<trans-unit id="Sign In">
<source>Sign In</source>
<target>Login</target>
</trans-unit>
<trans-unit id="Username">
<source>Username</source>
<target>Username</target>
</trans-unit>
<trans-unit id="Password">
<source>Password</source>
<target>Password</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="AdminLTEBundle.en.xliff">
<body>
<trans-unit id="Toggle navigation">
<source>Toggle navigation</source>
<target>Переключить отображение боковой панели</target>
</trans-unit>
<trans-unit id="Sign in to start your session">
<source>Sign in to start your session</source>
<target>Логин</target>
</trans-unit>
<trans-unit id="Sign In">
<source>Sign In</source>
<target>Войти</target>
</trans-unit>
<trans-unit id="Username">
<source>Username</source>
<target>Имя пользователя</target>
</trans-unit>
<trans-unit id="Password">
<source>Password</source>
<target>Пароль</target>
</trans-unit>
</body>
</file>
</xliff>

Binary file not shown.

View File

@@ -1,10 +1,77 @@
# Configurations
There are several configurations that can be configured with the yaml files in `config/packages/*.yaml``
Configuration of Kimai is spread in all files in the `config/`directory but mainly it these files:
Please read _Overwriting local configs_ to know where to store your own settings.
- `.env` - environment specific settings
- `config/packages/kimai.yaml` - Kimai specific settings
- `config/packages/admin_lte.yaml` - theme specific settings ([read more](https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/configurations.md))
- `config/packages/fos_user.yaml` - user management and email settings
- `config/packages/local.yaml` - your local configuration settings
## Duration only
There are several other configurations that could potentially be interesting for you in [config/packages/*.yaml](../../config/packages/).
If you want to adjust a setting from any of these files, use `local.yaml` (see below).
## Environment specific settings (.env)
The most basic settings, which need always be adjusted are stored in the `.env` file:
- `MAILER_URL` - smtp connection for emails
- `MAILER_FROM` - application specific "from" address for all emails
- `APP_ENV` - environment for the runtime (use `prod` if you are unsure)
- `DATABASE_URL` - database connection for storing all application data
- `DATABASE_PREFIX` - precix for any Kimai table in the configured database
- `APP_SECRET` - secret used for hasing user password (if you cahnge this, every password is invalid afterwards)
## Overwriting local configs (local.yaml)
You can create the file `config/packages/local.yaml` and store your own settings inside. This file will NEVER be shipped with Kimai.
So having your custom settings in this file allows you to easily update Kimai. This is the same concept which is used for the `.env` file.
An example `config/packages/local.yaml` file might look like this:
```yaml
kimai:
timesheet:
rounding:
default:
begin: 15
end: 15
admin_lte:
options:
default_avatar: build/apple-touch-icon.png
```
The `local.yaml` file will be imported as last configuration file, so you can overwrite any setting from the `config/packages/` directory.
After changing this file you have to clear the cache with `bin/console cache:clear` or `bin/console cache:clear --env=prod`.
## Emails (swiftmailer.yaml)
Kimai uses Swiftmailer to sent emails. You configure your SMTP connection setting in [.env](../../.env.sample).
For more configuration details read the [Swiftmailer](https://symfony.com/doc/current/reference/configuration/swiftmailer.html) documentation
and apply the settings in [swiftmailer.yaml](../../config/packages/swiftmailer.yaml).
## Security
Kimai uses the FOSUserBundle for security related tasks like user management. Its configuration can be found in [fos_user.yaml](../../config/packages/fos_user.yaml).
### User management emails (fos_user.yaml)
Kimai sents emails for newly registered users ([if that is configured](users.md)) and forgotten password requests.
You can change the contents of these emails by editing [fos_user.yaml](../../config/packages/fos_user.yaml).
You can find more information in the [FOSUserBundle](https://symfony.com/doc/master/bundles/FOSUserBundle/index.html) documentation.
### Remember me login (security.yaml)
The default period for the `Remember me` option can be changed in the config file [security.yaml](../../config/packages/security.yaml).
## Timesheets (kimai.yaml)
### Duration only
Kimai supports two modes for displaying and recording timesheet entries:
@@ -19,11 +86,7 @@ You can activate the `duration_only` mode by switching the configuration key `ki
For supported formats while entering the `duration` please see the [timesheet chapter](timesheet.md)
## Remember me login
The default period for the `Remember me` option can be changed in the config file [security.yaml](../../config/packages/security.yaml).
## Timesheet records - rounding of begin, end and duration
### Rounding of begin, end and duration for timesheet records
Rounding rules are used to round the begin & end dates and the duration for timesheet records.
@@ -38,7 +101,7 @@ Rounding rules are used to round the begin & end dates and the duration for time
You can configure your `rounding` rules by changing the configuration file [kimai.yaml](../../config/packages/kimai.yaml).
### Examples
#### Examples
A simple example to always charge at least 1 hour for weekend work (even if you only worked for 5 minutes) could look like this:
@@ -66,7 +129,7 @@ kimai:
duration: 0
```
## Timesheet records - hourly rates
### Hourly rates for timesheet records
If you want to apply different hourly rates multiplication `factor` for specific weekdays, you can use this `rates` configuration.
@@ -80,7 +143,7 @@ If you want to apply different hourly rates multiplication `factor` for specific
You can configure the `hourly_rate` rules by changing the configuration file [kimai.yaml](../../config/packages/kimai.yaml).
### Examples
#### Examples
1. The "workdays" rule will use the default "hourly rate" for each timesheet entry recorded between "monday" to "friday" as a multiplication with 1 will not change the result
2. The "weekend" rule will add 50% to each timesheet entry that will be recorded on "saturdays" or "sundays"
@@ -96,20 +159,3 @@ kimai:
days: ['saturday','sunday']
factor: 1.5
```
## Overwriting local configs
You can create the file `config/packages/local.yaml` and store your own settings inside. This file will NEVER be shipped with Kimai.
So having your custom settings in this file allows you to easily update Kimai. This is the same concept as used for the `.env` file.
An example `config/packages/local.yaml` file might look like this:
```yaml
kimai:
timesheet:
rounding:
default:
begin: 15
end: 15
```
After changing the file you have to clear the cache with `bin/console cache:clear` or `bin/console cache:clear --env=prod`.

View File

@@ -5,7 +5,7 @@ Internal documentation for project maintainers
## Create a release
- Prepare a GitHub release-draft
- Change .github_changelog_generator config accordingly to new release tag
- Change .github_changelog_generator config accordingly to new release tag (increase future release)
- Create CHANGELOG.md with [github-changelog-generator](https://github.com/github-changelog-generator/github-changelog-generator]) by running `github_changelog_generator kevinpapst/kimai2`
- 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

View File

@@ -12,12 +12,12 @@ There are multiple pre-defined roles in Kimai, which define the ACLs. A user can
| ROLE_ADMIN | ROLE_TEAMLEAD | All of the above, plus: editing customers, editing projects, editing activities |
| ROLE_SUPER_ADMIN | ROLE_ADMIN | All of the above, plus: editing users |
# Login & Authentication
## Login
- User can login with their username or email
- If you activate the `Remember me` option, you can use use the most common functions within the next days without a new login
## Remember me login
### Remember me login
If you have chosen to login with the `Remember me` option, your login will be extended to one week (default value).
After coming back and being remembered you have access to all the following features:
@@ -31,3 +31,28 @@ form when you try to access them. This is a security feature to prevent abuse in
environments.
Read the [configurations chapter](configurations.md) if you want to change the value.
## User registration
User registration with instant approval is activated by default, so users can register and will be able to login and start time-tracking instantly.
If you want your new users to use email based activation, then change the following configuration:
- in `config/packages/fos_user.yaml` change the setting `fos_user.registration.confirmation.enabled` to true (default: false)
If you want to deactivate the user registration completely, you have to change the following configs:
- in `config/packages/admin_lte.yaml` remove the route alias `admin_lte.routes.adminlte_registration` (this will remove the link from the login form)
- in `config/routes.yaml` remove the block `fos_user_registration` (this will deactivate the functionality)
## Password reset
The reset password function is enabled by default.
If you want to deactivate this feature you have to change the following configs:
- in `config/packages/admin_lte.yaml` remove the route alias `admin_lte.routes.adminlte_password_reset` (this will remove the link from the login form)
- in `config/routes.yaml` remove the block `fos_user_resetting` (this will deactivate the functionality)
If you want to configure the behaviour (like the allowed time between multiple retries) then configure the settings:
- in `config/packages/fos_user.yaml` the key below `fos_user.registration.resetting` (see [documentation](http://symfony.com/doc/current/bundles/FOSUserBundle/configuration_reference.html))
- the values `retry_ttl` and `token_ttl` are configured in seconds (7220 = 2 hours)

View File

@@ -2,9 +2,9 @@
# yarn lockfile v1
"@fortawesome/fontawesome-free@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.1.0.tgz#f35f5ba91366b7a58b0b6a4f22ff0907fe002219"
"@fortawesome/fontawesome-free@~5.1.1":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.1.1.tgz#b2c8d73675e349062112c2795a6e9e146bf47e1e"
"@symfony/webpack-encore@^0.20.1":
version "0.20.1"
@@ -93,7 +93,7 @@ adjust-sourcemap-loader@^1.1.0:
object-path "^0.9.2"
regex-parser "^2.2.9"
admin-lte@^2.3.11:
admin-lte@~2.3.11:
version "2.3.11"
resolved "https://registry.yarnpkg.com/admin-lte/-/admin-lte-2.3.11.tgz#da56dcd34d42e9ef1af362be15520a053a9d43db"
@@ -1092,6 +1092,10 @@ chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chart.js@1.1.*:
version "1.1.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-1.1.1.tgz#a9b17054220bd45cbdb176fd6bcb8783ef871a7d"
chokidar@^2.0.0, chokidar@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"