Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff6918fcab | ||
|
|
f376b5c8a1 | ||
|
|
b00fbd2516 | ||
|
|
d97abc3f57 | ||
|
|
3f184e42c8 | ||
|
|
4a31411d69 | ||
|
|
3925eacf9f | ||
|
|
394f377890 | ||
|
|
e8b0dc4969 |
@@ -1,17 +1,23 @@
|
||||
# editorconfig.org
|
||||
|
||||
; top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
; Unix-style newlines
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_size = 4
|
||||
|
||||
[{compose.yaml,compose.*.yaml}]
|
||||
indent_size = 2
|
||||
[*.twig]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
[*.yaml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
63
.env.dist
63
.env.dist
@@ -1,18 +1,7 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
# You should NOT use this file in production, but instead move the environment variables to your webserver configuration.
|
||||
# The .env file is only existing to simplify the initial setup!
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
#================================================================================
|
||||
# Configure your database connection and set the correct server version.
|
||||
@@ -27,34 +16,32 @@
|
||||
# For MySQL that would be "serverVersion=5.7" as in:
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
|
||||
#
|
||||
# For MariaDB it would be "serverVersion=10.11.15-MariaDB":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB
|
||||
# For MariaDB it would be "serverVersion=10.5.8-MariaDB":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
#
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
|
||||
#================================================================================
|
||||
# The full documentation can be found at https://www.kimai.org/documentation/emails.html
|
||||
#
|
||||
# Email will be sent with this address as sender:
|
||||
MAILER_FROM=kimai@example.com
|
||||
# Email connection (disabled by default) - see documentation for the format
|
||||
MAILER_URL=null://null
|
||||
|
||||
#================================================================================
|
||||
# do not change, unless you are developing for Kimai
|
||||
APP_ENV=prod
|
||||
|
||||
#================================================================================
|
||||
# should be changed to a unique character sequence, used for hashing cookies
|
||||
APP_SECRET=change_this_to_something_unique
|
||||
|
||||
#================================================================================
|
||||
# Running behind reverse proxies? Try these:
|
||||
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||
# TRUSTED_HOSTS=localhost|example.com
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=prod
|
||||
APP_SECRET=
|
||||
APP_SHARE_DIR=var/share
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
# Documentation at https://www.kimai.org/documentation/emails.html
|
||||
MAILER_FROM=kimai@example.com
|
||||
# Email connection (disabled by default) - see documentation for the format
|
||||
MAILER_URL=null://null
|
||||
###< symfony/mailer ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> symfony/routing ###
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
DEFAULT_URI=http://localhost
|
||||
###< symfony/routing ###
|
||||
#================================================================================
|
||||
# unlikely, that you need to change this one
|
||||
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -17,4 +17,4 @@ phpstan.neon export-ignore
|
||||
phpstan.sh export-ignore
|
||||
phpunit.xml.dist export-ignore
|
||||
webpack.config.js export-ignore
|
||||
yarn.lock export-ignore
|
||||
yarn.lock export-ignore
|
||||
5
.github/workflows/docker.yaml
vendored
5
.github/workflows/docker.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Determine version
|
||||
run: |
|
||||
input="${{ github.event.inputs.kimai_tag }}"
|
||||
|
||||
|
||||
# Determine between manual trigger and release event
|
||||
if [ -z "$input" ]; then
|
||||
echo "Using release tag: ${{ github.event.release.tag_name }}"
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
echo "Invalid version number: $version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
echo "kimai_version=$version" >> $GITHUB_ENV
|
||||
|
||||
- name: FPM image
|
||||
@@ -73,6 +73,7 @@ jobs:
|
||||
target: prod
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
kimai/kimai2:stable
|
||||
kimai/kimai2:apache
|
||||
kimai/kimai2:apache-${{ env.kimai_version }}
|
||||
push: true
|
||||
|
||||
2
.github/workflows/testing.yaml
vendored
2
.github/workflows/testing.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.4', '8.5']
|
||||
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
|
||||
|
||||
name: Integration (${{ matrix.php }})
|
||||
steps:
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# some hosters require a htaccess to change the PHP version
|
||||
/public/.htaccess
|
||||
/.env-*
|
||||
/.idea/
|
||||
.DS_Store
|
||||
var/templates/
|
||||
@@ -23,7 +24,6 @@ var/templates/
|
||||
/var/dev/*
|
||||
/var/data/*
|
||||
/var/cache/*
|
||||
/var/share/*
|
||||
/var/invoices*
|
||||
/var/export*
|
||||
/var/log/*
|
||||
@@ -37,6 +37,7 @@ var/templates/
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
.env
|
||||
/public/bundles/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
@@ -47,8 +48,9 @@ var/templates/
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
|
||||
13
README.md
13
README.md
@@ -6,11 +6,9 @@
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/actions/workflows/testing.yaml/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
|
||||
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
|
||||
<a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Kimai - time-tracker</h1>
|
||||
<h1 align="center">Kimai<br>#1 Open-Source Time-Tracker</h1>
|
||||
|
||||
Kimai is a professional grade time-tracking application, free and open-source.
|
||||
It handles use-cases of freelancers as well as companies with dozens or hundreds of users.
|
||||
@@ -30,7 +28,7 @@ and so much more.
|
||||
### Requirements
|
||||
|
||||
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5
|
||||
- MariaDB or MySQL
|
||||
- MariaDB / MySQL in their oldest maintained LTS release, currently MariaDB >= [10.6](https://endoflife.date/mariadb) or MySQL >= [8.4](https://endoflife.date/mysql)
|
||||
- A webserver and subdomain (subdirectory is not supported)
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
||||
|
||||
@@ -71,6 +69,7 @@ The best way to start is to [open a new issue](https://github.com/kimai/kimai/is
|
||||
In case you want to contribute, but you wouldn't know how, here are some suggestions:
|
||||
|
||||
- Spread the word: Please [write a testimonial for our Wall of love](https://love.kimai.org), vote for Kimai on any software platform, you can toot or tweet about it, share it on LinkedIn, Reddit and any other social media platform!
|
||||
- [Translate Kimai into your language](https://hosted.weblate.org/engage/kimai/), or help to improve the existing translations, many languages look for a contributor
|
||||
- Answer questions: You know the answer to another user's problem? Share your knowledge.
|
||||
- Something can be done better? An essential feature is missing? Create a feature request.
|
||||
- Report bugs makes Kimai better for everyone.
|
||||
@@ -79,6 +78,12 @@ In case you want to contribute, but you wouldn't know how, here are some suggest
|
||||
|
||||
There is one simple rule in our "Code of conduct": Don't be an ass!
|
||||
|
||||
## Follow Kimai
|
||||
|
||||
- Mastodon: [@kimai](https://phpc.social/@kimai)
|
||||
- Youtube: [@kimai_org](https://www.youtube.com/@kimai_org)
|
||||
- LinkedIn: [@kimai-org](https://www.linkedin.com/company/kimai-org/)
|
||||
|
||||
### Credits
|
||||
|
||||
Kimai is based on modern technologies and frameworks such as [PHP](https://www.php.net/),
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Upgrading Kimai - Version 2.x
|
||||
|
||||
_Make sure to create a backup before you start!_
|
||||
|
||||
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
|
||||
you can upgrade your Kimai installation to the latest stable release.
|
||||
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
**!! This release requires minimum PHP version to 8.1 !!**
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
|
||||
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
|
||||
- removed: configuring the `dashboard` is not supported any longer
|
||||
- removed: custom translation files via `theme.branding.translation`
|
||||
- removed: changing the plugin directory via `kimai.plugin_dir`
|
||||
|
||||
### Developer
|
||||
|
||||
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
|
||||
|
||||
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
|
||||
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
|
||||
- Role names are forced to be uppercase
|
||||
- Removed unused `public/avatars/` directory
|
||||
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
|
||||
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
|
||||
- `date_full` => `date_time`
|
||||
- `duration_decimal` => `duration(true)`
|
||||
- `currency` => `currency_name`
|
||||
- `country` => `country_name`
|
||||
- `language` => `language_name`
|
||||
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
|
||||
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
|
||||
- `composer require symfony/amazon-mailer`
|
||||
- `composer require symfony/google-mailer`
|
||||
- `composer require symfony/mailchimp-mailer`
|
||||
- `composer require symfony/mailgun-mailer`
|
||||
- `composer require symfony/postmark-mailer`
|
||||
- `composer require symfony/sendgrid-mailer`
|
||||
@@ -11,27 +11,16 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
|
||||
**!! This release requires minimum PHP version 8.4 !!**
|
||||
|
||||
### Rename .env
|
||||
|
||||
Rename your file `.env` to `.env.local` or even better: move all variables to your webserver/container environment.
|
||||
|
||||
### Developer
|
||||
|
||||
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
|
||||
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
|
||||
|
||||
- Require PHP 8.4
|
||||
- Bump to Symfony 7.4
|
||||
- Removed old API token `X-AUTH-USER` and `X-AUTH-TOKEN`
|
||||
- Removed `TimesheetConstraint` - use a normal `Constraint` as base class and attach the `#[App\Validator\Attribute\TimesheetConstraint]` attribute
|
||||
- Removed `ProjectConstraint` - use a `FormExtension` and attach your custom constraints
|
||||
- Interface `MetaTableTypeInterface` has new methods: `getSection()`, `setSection()`
|
||||
- Interface `ExportRendererInterface` has new methods: `getType()`, `isInternal()`
|
||||
- Interface `ExportableItem` has new methods: `getTags()`, `getBreak()`
|
||||
- Removed and renamed translations, most important `action.edit` => `edit`, `my.profile` => `user_profile`
|
||||
- Removed `User::isExportDecimal()`
|
||||
- Use duration format `HH:mm` in default PDF exports
|
||||
- Replace Twig `AppVariable` with custom implementation
|
||||
- You need to adjust your templates if you access anything else then `app.locale`, `app.user`. `app.current_route`.
|
||||
- Most often used:
|
||||
- Replace `app.request.locale` with `app.locale`
|
||||
- Replace `app.request.attributes.get('_route')` with `app.current_route`
|
||||
Removed translations:
|
||||
- `action.edit`: use `edit` instead
|
||||
- `my.profile`: use `user_profile` instead
|
||||
- `stats.userAmountToday`: use `` instead
|
||||
- `stats.userAmountWeek`: use `` instead
|
||||
- `stats.userAmountMonth`: use `` instead
|
||||
- `stats.userAmountYear`: use `` instead
|
||||
- `stats.userAmountTotal`: use `` instead
|
||||
- `update_multiple`
|
||||
51
UPGRADING.md
51
UPGRADING.md
@@ -1,3 +1,50 @@
|
||||
# Upgrading Kimai
|
||||
# Upgrading Kimai - Version 2.x
|
||||
|
||||
The most recent version is Kimai 3, please read the changelog at [UPGRADING-3.md](UPGRADING-3.md).
|
||||
_Make sure to create a backup before you start!_
|
||||
|
||||
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
|
||||
you can upgrade your Kimai installation to the latest stable release.
|
||||
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
**!! This release requires minimum PHP version to 8.1 !!**
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
|
||||
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
|
||||
- removed: configuring the `dashboard` is not supported any longer
|
||||
- removed: custom translation files via `theme.branding.translation`
|
||||
- removed: changing the plugin directory via `kimai.plugin_dir`
|
||||
|
||||
### Developer
|
||||
|
||||
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
|
||||
|
||||
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
|
||||
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
|
||||
- Role names are forced to be uppercase
|
||||
- Removed unused `public/avatars/` directory
|
||||
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
|
||||
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
|
||||
- `date_full` => `date_time`
|
||||
- `duration_decimal` => `duration(true)`
|
||||
- `currency` => `currency_name`
|
||||
- `country` => `country_name`
|
||||
- `language` => `language_name`
|
||||
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
|
||||
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
|
||||
- `composer require symfony/amazon-mailer`
|
||||
- `composer require symfony/google-mailer`
|
||||
- `composer require symfony/mailchimp-mailer`
|
||||
- `composer require symfony/mailgun-mailer`
|
||||
- `composer require symfony/postmark-mailer`
|
||||
- `composer require symfony/sendgrid-mailer`
|
||||
|
||||
@@ -43,7 +43,6 @@ import KimaiHotkeys from "./plugins/KimaiHotkeys";
|
||||
import KimaiRemoteModal from "./plugins/KimaiRemoteModal";
|
||||
import KimaiUser from "./plugins/KimaiUser";
|
||||
import KimaiAutocompleteTags from "./forms/KimaiAutocompleteTags";
|
||||
import KimaiMonthPicker from "./forms/KimaiMonthPicker";
|
||||
|
||||
export default class KimaiLoader {
|
||||
|
||||
@@ -77,7 +76,6 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiTeamForm());
|
||||
kimai.registerPlugin(new KimaiCopyDataForm());
|
||||
kimai.registerPlugin(new KimaiDateNowForm());
|
||||
kimai.registerPlugin(new KimaiMonthPicker());
|
||||
kimai.registerPlugin(new KimaiForm());
|
||||
kimai.registerPlugin(new KimaiHotkeys());
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiDatePicker: single date selects (currently unused)
|
||||
*/
|
||||
|
||||
import KimaiFormPlugin from "./KimaiFormPlugin";
|
||||
|
||||
export default class KimaiMonthPicker extends KimaiFormPlugin {
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
* @return boolean
|
||||
*/
|
||||
supportsForm(form) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
*/
|
||||
activateForm(form)
|
||||
{
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type','month');
|
||||
|
||||
const notADateValue = 'not-a-month';
|
||||
input.setAttribute('value', notADateValue);
|
||||
|
||||
if (input.value === notADateValue) {
|
||||
const polyfills = form.querySelectorAll('a.input-month-polyfill');
|
||||
polyfills.forEach(i => i.classList.toggle('d-none'));
|
||||
const inputs = form.querySelectorAll('input[type="month"]');
|
||||
inputs.forEach(i => i.classList.toggle('d-none'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "variables";
|
||||
@import "layout";
|
||||
@import "error-page";
|
||||
@import "print";
|
||||
|
||||
33
assets/sass/variables.scss
Normal file
33
assets/sass/variables.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
:root {
|
||||
--kimai-public-holiday: var(--tblr-lime);
|
||||
--kimai-holiday: var(--tblr-green);
|
||||
--kimai-sickness: var(--tblr-yellow);
|
||||
--kimai-time-off: var(--tblr-blue);
|
||||
--kimai-other: var(--tblr-purple);
|
||||
|
||||
--kimai-public-holiday-bg: var(--tblr-lime-lt);
|
||||
--kimai-holiday-bg: var(--tblr-green-lt);
|
||||
--kimai-sickness-bg: var(--tblr-yellow-lt);
|
||||
--kimai-time-off-bg: var(--tblr-blue-lt);
|
||||
--kimai-other-bg: var(--tblr-purple-lt);
|
||||
|
||||
--kimai-unexpected-bg: var(--tblr-pink-lt);
|
||||
--kimai-missing-bg: var(--tblr-pink-lt);
|
||||
--kimai-weekend-bg: var(--tblr-bg-surface-tertiary);
|
||||
}
|
||||
|
||||
.public-holiday { color: var(--kimai-public-holiday); }
|
||||
.holiday { color: var(--kimai-holiday); }
|
||||
.sickness { color: var(--kimai-sickness); }
|
||||
.time-off { color: var(--kimai-time-off); }
|
||||
.other { color: var(--kimai-other); }
|
||||
|
||||
.bg-public-holiday{ background-color: var(--kimai-public-holiday-bg); --tblr-table-bg: var(--kimai-public-holiday-bg); i.fas{ color: var(--kimai-public-holiday); } };
|
||||
.bg-holiday { background-color: var(--kimai-holiday-bg); --tblr-table-bg: var(--kimai-holiday-bg); i.fas{ color: var(--kimai-holiday); } };
|
||||
.bg-sickness { background-color: var(--kimai-sickness-bg); --tblr-table-bg: var(--kimai-sickness-bg); i.fas{ color: var(--kimai-sickness); } };
|
||||
.bg-time-off { background-color: var(--kimai-time-off-bg); --tblr-table-bg: var(--kimai-time-off-bg); i.fas{ color: var(--kimai-time-off); } };
|
||||
.bg-other { background-color: var(--kimai-other-bg); --tblr-table-bg: var(--kimai-other-bg); i.fas{ color: var(--kimai-other); } };
|
||||
|
||||
.bg-unexpected { background-color: var(--kimai-unexpected-bg); --tblr-table-bg: var(--kimai-unexpected-bg); };
|
||||
.bg-missing { background-color: var(--kimai-missing-bg); --tblr-table-bg: var(--kimai-missing-bg); };
|
||||
.bg-weekend { background-color: var(--kimai-weekend-bg); --tblr-table-bg: var(--kimai-weekend-bg); };
|
||||
@@ -6,8 +6,8 @@ use App\ConsoleApplication;
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "8.4.*||8.5.*",
|
||||
"php": "8.1.*||8.2.*||8.3.*||8.4.*||8.5.*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
@@ -29,10 +29,11 @@
|
||||
"composer/semver": "^3.3",
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^3.0",
|
||||
"doctrine/orm": "^2.8",
|
||||
"easybill/zugferd-php": "^2.1",
|
||||
"endroid/qr-code": "^6.0",
|
||||
"friendsofsymfony/rest-bundle": "3.9.0-beta1",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"horstoeko/zugferd": "^1.0",
|
||||
"horstoeko/zugferdublbridge": "^1.0",
|
||||
@@ -44,39 +45,37 @@
|
||||
"nelmio/cors-bundle": "^2.0",
|
||||
"onelogin/php-saml": "^4.0",
|
||||
"openspout/openspout": "^4.0",
|
||||
"pagerfanta/pagerfanta": "^4.0",
|
||||
"parsedown/parsedown": "^1.6",
|
||||
"pagerfanta/pagerfanta": "^3.0",
|
||||
"phpoffice/phpspreadsheet": "^2.0",
|
||||
"phpoffice/phpword": "^1.0",
|
||||
"psr/container": "^2.0",
|
||||
"psr/log": "^3.0",
|
||||
"scheb/2fa-backup-code": "^7.11",
|
||||
"scheb/2fa-bundle": "^7.11",
|
||||
"scheb/2fa-totp": "^7.11",
|
||||
"symfony/asset": "^7.0",
|
||||
"symfony/console": "^7.0",
|
||||
"symfony/dependency-injection": "^7.0",
|
||||
"symfony/dotenv": "^7.0",
|
||||
"symfony/expression-language": "^7.0",
|
||||
"scheb/2fa-backup-code": "^6.2",
|
||||
"scheb/2fa-bundle": "^6.2",
|
||||
"scheb/2fa-totp": "^6.2",
|
||||
"symfony/asset": "^6.0",
|
||||
"symfony/console": "^6.0",
|
||||
"symfony/dotenv": "^6.0",
|
||||
"symfony/expression-language": "^6.0",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "^7.0",
|
||||
"symfony/framework-bundle": "^7.0",
|
||||
"symfony/http-client": "^7.0",
|
||||
"symfony/intl": "^7.0",
|
||||
"symfony/mailer": "^7.0",
|
||||
"symfony/mime": "^7.0",
|
||||
"symfony/form": "^6.0",
|
||||
"symfony/framework-bundle": "^6.0",
|
||||
"symfony/http-client": "^6.0",
|
||||
"symfony/intl": "^6.0",
|
||||
"symfony/mailer": "^6.0",
|
||||
"symfony/mime": "^6.0",
|
||||
"symfony/monolog-bundle": "^3.4",
|
||||
"symfony/process": "^7.0",
|
||||
"symfony/rate-limiter": "^7.0",
|
||||
"symfony/runtime": "^7.0",
|
||||
"symfony/security-bundle": "^7.0",
|
||||
"symfony/security-csrf": "^7.0",
|
||||
"symfony/serializer": "^7.0",
|
||||
"symfony/translation": "^7.0",
|
||||
"symfony/twig-bundle": "^7.0",
|
||||
"symfony/validator": "^7.0",
|
||||
"symfony/process": "^6.0",
|
||||
"symfony/rate-limiter": "^6.0",
|
||||
"symfony/runtime": "^6.0",
|
||||
"symfony/security-bundle": "^6.0",
|
||||
"symfony/security-csrf": "^6.0",
|
||||
"symfony/serializer": "^6.0",
|
||||
"symfony/translation": "^6.0",
|
||||
"symfony/twig-bundle": "^6.0",
|
||||
"symfony/validator": "^6.0",
|
||||
"symfony/webpack-encore-bundle": "^2.0",
|
||||
"symfony/yaml": "^7.0",
|
||||
"symfony/yaml": "^6.0",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
"twig/inky-extra": "^3.0",
|
||||
@@ -96,14 +95,14 @@
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"symfony/browser-kit": "^7.0",
|
||||
"symfony/css-selector": "^7.0",
|
||||
"symfony/debug-bundle": "^7.0",
|
||||
"symfony/dom-crawler": "^7.0",
|
||||
"symfony/phpunit-bridge": "^7.0",
|
||||
"symfony/stopwatch": "^7.0",
|
||||
"symfony/var-dumper": "^7.0",
|
||||
"symfony/web-profiler-bundle": "^7.0"
|
||||
"symfony/browser-kit": "^6.0",
|
||||
"symfony/css-selector": "^6.0",
|
||||
"symfony/debug-bundle": "^6.0",
|
||||
"symfony/dom-crawler": "^6.0",
|
||||
"symfony/phpunit-bridge": "^6.0",
|
||||
"symfony/stopwatch": "^6.0",
|
||||
"symfony/var-dumper": "^6.0",
|
||||
"symfony/web-profiler-bundle": "^6.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
@@ -118,7 +117,7 @@
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"platform": {
|
||||
"php": "8.4.4"
|
||||
"php": "8.1.3"
|
||||
},
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
@@ -145,9 +144,6 @@
|
||||
"symfony/polyfill-intl-idn": "*",
|
||||
"symfony/polyfill-intl-normalizer": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php84": "*",
|
||||
"symfony/polyfill-php83": "*",
|
||||
"symfony/polyfill-php82": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
@@ -213,7 +209,7 @@
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "7.4.*"
|
||||
"require": "6.4.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3130
composer.lock
generated
3130
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ return [
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
|
||||
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
6
config/packages/assets.yaml
Normal file
6
config/packages/assets.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
packages:
|
||||
avatars:
|
||||
base_path: 'avatars'
|
||||
17
config/packages/cache.yaml
Normal file
17
config/packages/cache.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
framework:
|
||||
cache:
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://127.0.0.1:6379
|
||||
|
||||
#app: cache.adapter.memcached
|
||||
#default_memcached_provider: 'memcached://localhost'
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
@@ -1,5 +0,0 @@
|
||||
when@test:
|
||||
dama_doctrine_test:
|
||||
enable_static_connection: true
|
||||
enable_static_meta_data_cache: true
|
||||
enable_static_query_cache: true
|
||||
@@ -1,5 +0,0 @@
|
||||
when@dev:
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
||||
@@ -10,31 +10,29 @@ doctrine:
|
||||
default_connection: default
|
||||
connections:
|
||||
default:
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
# existing migrations will fail if the schema filter is activated
|
||||
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
url: '%env(DATABASE_URL)%'
|
||||
driver: 'pdo_mysql'
|
||||
charset: utf8mb4
|
||||
default_table_options:
|
||||
charset: utf8mb4
|
||||
collation: utf8mb4_unicode_ci
|
||||
schema_manager_factory: doctrine.dbal.default_schema_manager_factory
|
||||
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
datetime_immutable: App\Doctrine\UTCDateTimeImmutableType
|
||||
orm:
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
auto_generate_proxy_classes: true
|
||||
# FIXME this is causing a deprecation and needs to be changed
|
||||
# auto_mapping: false
|
||||
auto_mapping: true
|
||||
auto_generate_proxy_classes: '%kernel.debug%'
|
||||
default_entity_manager: default
|
||||
enable_native_lazy_objects: true
|
||||
enable_lazy_ghost_objects: true
|
||||
entity_managers:
|
||||
default:
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
validate_xml_mapping: true
|
||||
report_fields_where_declared: true
|
||||
connection: default
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
@@ -58,23 +56,17 @@ when@test:
|
||||
connections:
|
||||
default:
|
||||
logging: false
|
||||
use_savepoints: true
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
metadata_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
transactional: false
|
||||
storage:
|
||||
table_storage:
|
||||
table_name: 'migration_versions'
|
||||
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'
|
||||
migrations_paths:
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'
|
||||
@@ -1,4 +1,3 @@
|
||||
# Read the documentation: https://fosrestbundle.readthedocs.io/en/3.x/
|
||||
fos_rest:
|
||||
param_fetcher_listener:
|
||||
enabled: true
|
||||
|
||||
@@ -37,9 +37,6 @@ framework:
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
|
||||
mailer:
|
||||
dsn: '%env(MAILER_URL)%'
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ jms_serializer:
|
||||
datetime:
|
||||
default_format: 'Y-m-d\TH:i:sO' # DATE_ISO8601
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
xml_serialization:
|
||||
format_output: '%kernel.debug%'
|
||||
metadata:
|
||||
@@ -14,20 +18,3 @@ jms_serializer:
|
||||
excluded: []
|
||||
property_naming:
|
||||
id: 'jms_serializer.identical_property_naming_strategy'
|
||||
|
||||
when@prod:
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
|
||||
when@dev:
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_PRETTY_PRINT
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
|
||||
@@ -1,50 +1,5 @@
|
||||
monolog:
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: notice
|
||||
handler: file_log
|
||||
excluded_http_codes:
|
||||
- {400: ['^/api/']}
|
||||
- {401: ['^/api/']}
|
||||
- 403
|
||||
- 404
|
||||
- 405
|
||||
channels: ["!event", "!deprecation"]
|
||||
file_log:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
formatter: monolog.formatter.kimai
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!deprecation"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: ["deprecation"]
|
||||
path: "%kernel.logs_dir%/deprecations.log"
|
||||
formatter: monolog.formatter.deprecation
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
formatter: monolog.formatter.kimai
|
||||
channels: ["deprecation"]
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
@@ -69,3 +24,42 @@ when@prod:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!deprecation"]
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: notice
|
||||
handler: file_log
|
||||
excluded_http_codes:
|
||||
- {400: ['^/api/']}
|
||||
- {401: ['^/api/']}
|
||||
- 403
|
||||
- 404
|
||||
- {405: ['/homepage$', '/login_check$', '/export/data$']}
|
||||
channels: ["!event", "!deprecation"]
|
||||
file_log:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
formatter: monolog.formatter.kimai
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!deprecation"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: ["deprecation"]
|
||||
path: "%kernel.logs_dir%/deprecations.log"
|
||||
formatter: monolog.formatter.deprecation
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: info
|
||||
channels: ["!event"]
|
||||
formatter: monolog.formatter.kimai
|
||||
|
||||
@@ -17,7 +17,7 @@ nelmio_cors:
|
||||
'^/api/':
|
||||
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_origin: ['*']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
allow_headers: ['Content-Type', 'Authorization', 'X-AUTH-USER', 'X-AUTH-TOKEN']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
# expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
parameters:
|
||||
env(default_uri): 'http://localhost'
|
||||
|
||||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
@@ -8,7 +5,7 @@ framework:
|
||||
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
default_uri: '%env(DEFAULT_URI)%'
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
App\Entity\User: auto
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
|
||||
providers:
|
||||
chain_provider:
|
||||
chain:
|
||||
@@ -12,11 +11,12 @@ security:
|
||||
class: App\Entity\User
|
||||
kimai_ldap:
|
||||
id: App\Ldap\LdapUserProvider
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
# Ensure dev tools and static assets are always allowed
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
|
||||
api:
|
||||
access_token:
|
||||
token_handler: App\API\Authentication\AccessTokenHandler
|
||||
@@ -27,52 +27,66 @@ security:
|
||||
stateless: true
|
||||
remember_me: false
|
||||
provider: chain_provider
|
||||
custom_authenticators:
|
||||
- App\API\Authentication\TokenAuthenticator
|
||||
|
||||
secured_area:
|
||||
kimai_ldap: ~
|
||||
pattern: ^/
|
||||
user_checker: App\Security\UserChecker
|
||||
stateless: false
|
||||
|
||||
entry_point: form_login
|
||||
|
||||
custom_authenticators:
|
||||
- App\Saml\SamlAuthenticator
|
||||
|
||||
remember_me:
|
||||
name: KIMAI_REMEMBER
|
||||
secret: '%kernel.secret%'
|
||||
lifetime: 604800
|
||||
path: /
|
||||
always_remember_me: true
|
||||
|
||||
# activate all configured user provider
|
||||
provider: chain_provider
|
||||
|
||||
form_login:
|
||||
check_path: security_check
|
||||
login_path: login
|
||||
enable_csrf: true
|
||||
|
||||
two_factor:
|
||||
auth_form_path: 2fa_login
|
||||
check_path: 2fa_login_check
|
||||
remember_me_sets_trusted: true
|
||||
|
||||
logout:
|
||||
path: logout
|
||||
target: homepage
|
||||
enable_csrf: false
|
||||
|
||||
login_throttling:
|
||||
max_attempts: 5
|
||||
interval: '5 minutes'
|
||||
|
||||
login_link:
|
||||
check_route: link_login_check
|
||||
signature_properties: ['id']
|
||||
lifetime: 900
|
||||
max_uses: 3
|
||||
|
||||
access_decision_manager:
|
||||
# only grants access if there is no voter denying access
|
||||
strategy: unanimous
|
||||
allow_if_all_abstain: false
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_USER: ~
|
||||
ROLE_TEAMLEAD: ROLE_USER
|
||||
ROLE_ADMIN: ROLE_TEAMLEAD
|
||||
ROLE_SUPER_ADMIN: ROLE_ADMIN
|
||||
# Note: Only the *first* matching rule is applied
|
||||
|
||||
access_control:
|
||||
- {path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS}
|
||||
- {path: '^/auth', roles: PUBLIC_ACCESS}
|
||||
@@ -85,12 +99,13 @@ security:
|
||||
- {path: '^/api', roles: IS_AUTHENTICATED}
|
||||
|
||||
when@test:
|
||||
# this configuration simplifies testing URLs protected by the security mechanism
|
||||
# See https://symfony.com/doc/current/cookbook/testing/http_authentication.html
|
||||
security:
|
||||
password_hashers:
|
||||
# Password hashers are resource-intensive by design to ensure security.
|
||||
# In tests, it's safe to reduce their cost to improve performance.
|
||||
App\Entity\User:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
# see https://github.com/symfony/recipes/pull/1026
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
|
||||
@@ -75,6 +75,7 @@ tabler:
|
||||
fax: fas fa-fax
|
||||
filter: fas fa-filter
|
||||
help: far fa-question-circle
|
||||
holiday: fas fa-umbrella-beach
|
||||
home: fas fa-home
|
||||
info: fas fa-info-circle
|
||||
import: fas fa-file-import
|
||||
@@ -95,6 +96,7 @@ tabler:
|
||||
ods: fas fa-table
|
||||
off: fas fa-toggle-off
|
||||
on: fas fa-toggle-on
|
||||
other: fas fa-file-alt
|
||||
password: fas fa-key
|
||||
pause: fas fa-pause
|
||||
pause-small: far fa-pause-circle
|
||||
@@ -108,6 +110,7 @@ tabler:
|
||||
profile: fas fa-user-edit
|
||||
profile-stats: far fa-chart-bar
|
||||
project: fas fa-briefcase
|
||||
public-holiday: fas fa-calendar-day
|
||||
repeat: fas fa-repeat
|
||||
reporting: far fa-chart-bar
|
||||
report: far fa-chart-bar
|
||||
@@ -119,6 +122,7 @@ tabler:
|
||||
save: far fa-save
|
||||
search: fas fa-search
|
||||
settings: fas fa-cog
|
||||
sickness: fas fa-prescription-bottle-medical
|
||||
shop: fas fa-shopping-cart
|
||||
spinner: fas fa-spinner
|
||||
start: fas fa-play
|
||||
@@ -130,6 +134,7 @@ tabler:
|
||||
team: fas fa-users
|
||||
timesheet: fas fa-clock
|
||||
timesheet-team: fas fa-user-clock
|
||||
time-off: fas fa-couch
|
||||
trash: far fa-trash-alt
|
||||
unlocked: fas fa-unlock-alt
|
||||
upload: fas fa-upload
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
debug: '%kernel.debug%'
|
||||
strict_variables: '%kernel.debug%'
|
||||
form_themes:
|
||||
- 'form/vertical.html.twig'
|
||||
exception_controller: null
|
||||
paths:
|
||||
'%kernel.project_dir%/templates/bundles/TablerBundle': theme
|
||||
'%kernel.project_dir%/vendor/kevinpapst/tabler-bundle/templates': theme
|
||||
globals:
|
||||
app: '@App\Twig\AppVariable'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
profiler: { only_exceptions: false }
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect: false
|
||||
collect_serializer_data: true
|
||||
profiler: { collect: false }
|
||||
|
||||
@@ -1,45 +1,17 @@
|
||||
webpack_encore:
|
||||
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
# The path where Encore is building the assets.
|
||||
# This should match Encore.setOutputPath() in webpack.config.js.
|
||||
output_path: '%kernel.project_dir%/public/build'
|
||||
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||
# output_path: false
|
||||
|
||||
# Set attributes that will be rendered on all script and link tags
|
||||
script_attributes:
|
||||
defer: true
|
||||
# Uncomment (also under link_attributes) if using Turbo Drive
|
||||
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
|
||||
# 'data-turbo-track': reload
|
||||
# link_attributes:
|
||||
# Uncomment if using Turbo Drive
|
||||
# 'data-turbo-track': reload
|
||||
|
||||
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# if using Encore.enableIntegrityHashes() specify the crossorigin attribute value (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# crossorigin: 'anonymous'
|
||||
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Available in version 1.2
|
||||
cache: '%kernel.debug%'
|
||||
|
||||
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
|
||||
# preload: true
|
||||
|
||||
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
|
||||
# strict_mode: false
|
||||
|
||||
# If you have multiple builds:
|
||||
# builds:
|
||||
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||
|
||||
# pass the build name as the 3rd argument to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
|
||||
when@prod:
|
||||
when@dev:
|
||||
webpack_encore:
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Available in version 1.2
|
||||
cache: true
|
||||
|
||||
#when@test:
|
||||
# webpack_encore:
|
||||
# strict_mode: false
|
||||
cache: false
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
||||
|
||||
# This file is the entry point to configure the routes of your app.
|
||||
# Methods with the #[Route] attribute are automatically imported.
|
||||
# See also https://symfony.com/doc/current/routing.html
|
||||
|
||||
# To list all registered routes, run the following command:
|
||||
# bin/console debug:router
|
||||
|
||||
controllers:
|
||||
# resource: routing.controllers
|
||||
resource: ../src/Controller/
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
@@ -18,8 +8,7 @@ controllers:
|
||||
api.swagger_ui:
|
||||
path: /api/doc
|
||||
methods: GET
|
||||
defaults:
|
||||
_controller: nelmio_api_doc.controller.stoplight
|
||||
defaults: { _controller: nelmio_api_doc.controller.stoplight }
|
||||
|
||||
api:
|
||||
resource: ../src/API/
|
||||
|
||||
5
config/routes/dev/twig.yaml
Normal file
5
config/routes/dev/twig.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /{_locale}/_error
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
7
config/routes/dev/web_profiler.yaml
Normal file
7
config/routes/dev/web_profiler.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
@@ -1,6 +0,0 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||
prefix: /{_locale}/_error
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
@@ -1,3 +0,0 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
@@ -1,8 +0,0 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||
prefix: /_profiler
|
||||
@@ -1,11 +1,3 @@
|
||||
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
||||
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
# See also https://symfony.com/doc/current/service_container/import.html
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
# can be regenerated with: bin/console kimai:reset:locales
|
||||
@@ -14,8 +6,8 @@ parameters:
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
public: false
|
||||
bind:
|
||||
$projectDirectory: '%kernel.project_dir%'
|
||||
@@ -24,11 +16,12 @@ services:
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
resource: '../src/*'
|
||||
exclude:
|
||||
- '../src/API/Model/'
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Doctrine/Extensions/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Event/'
|
||||
- '../src/Export/Package/'
|
||||
- '../src/Export/Base/'
|
||||
@@ -42,9 +35,6 @@ services:
|
||||
- '../src/Kernel.php'
|
||||
- '../src/Constants.php'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
App\Controller\:
|
||||
@@ -237,13 +227,3 @@ services:
|
||||
|
||||
App\API\ViewHandler:
|
||||
arguments: ['@fos_rest.view_handler.default']
|
||||
|
||||
App\Validator\Constraints\TimesheetAllValidator:
|
||||
arguments: ['%kimai.validator_timesheet%']
|
||||
|
||||
App\Validator\Constraints\QuickEntryTimesheetValidator:
|
||||
arguments: ['%kimai.validator_timesheet%']
|
||||
|
||||
App\Twig\AppVariable:
|
||||
tags:
|
||||
- {name: twig.global, alias: app}
|
||||
|
||||
25
config/services_test.yaml
Normal file
25
config/services_test.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
parameters:
|
||||
locale: en
|
||||
kimai_locales: ['ar', 'cs', 'de', 'de_CH', 'da', 'en', 'eo', 'es', 'eu', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pl', 'pt_BR', 'ro', 'ru', 'sk', 'sv', 'tr', 'vi', 'zh_CN']
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
public: true
|
||||
|
||||
App\Configuration\SystemConfiguration:
|
||||
arguments: ['@App\Configuration\ConfigurationService', "%kimai.config%"]
|
||||
|
||||
# required for the importer command test
|
||||
App\Repository\UserRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\User']
|
||||
|
||||
# required for the importer command test
|
||||
App\Importer\ImporterService:
|
||||
public: true
|
||||
arguments: ['@App\Customer\CustomerService', '@App\Project\ProjectService']
|
||||
|
||||
App\User\UserService:
|
||||
public: true
|
||||
arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.user_password_hasher']
|
||||
0
migrations/.gitignore
vendored
0
migrations/.gitignore
vendored
@@ -28,13 +28,13 @@ class Version20190510205245 extends AbstractMigration
|
||||
$timesheetTags->addColumn('tag_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$timesheetTags->addIndex(['timesheet_id'], 'IDX_E3284EFEABDD46BE');
|
||||
$timesheetTags->addIndex(['tag_id'], 'IDX_E3284EFEBAD26311');
|
||||
$this->addPrimaryKeyConstraint($timesheetTags, ['timesheet_id', 'tag_id']);
|
||||
$timesheetTags->setPrimaryKey(['timesheet_id', 'tag_id']);
|
||||
|
||||
$tags = $schema->createTable('kimai2_tags');
|
||||
$tags->addColumn('id', 'integer', ['length' => 11, 'autoincrement' => true, 'notnull' => true]);
|
||||
$tags->addColumn('name', 'string', ['length' => 100, 'notnull' => true]);
|
||||
$tags->addUniqueIndex(['name'], 'UNIQ_27CAF54C5E237E06');
|
||||
$this->addPrimaryKeyConstraint($tags, ['id']);
|
||||
$tags->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
|
||||
@@ -34,7 +34,7 @@ final class Version20190617100845 extends AbstractMigration
|
||||
$timesheetMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$timesheetMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$timesheetMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($timesheetMeta, ['id']);
|
||||
$timesheetMeta->setPrimaryKey(['id']);
|
||||
$timesheetMeta->addIndex(['timesheet_id'], 'IDX_CB606CBAABDD46BE');
|
||||
$timesheetMeta->addUniqueIndex(['timesheet_id', 'name'], 'UNIQ_CB606CBAABDD46BE5E237E06');
|
||||
$timesheetMeta->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_CB606CBAABDD46BE');
|
||||
@@ -45,7 +45,7 @@ final class Version20190617100845 extends AbstractMigration
|
||||
$customerMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$customerMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$customerMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($customerMeta, ['id']);
|
||||
$customerMeta->setPrimaryKey(['id']);
|
||||
$customerMeta->addIndex(['customer_id'], 'IDX_A48A760F9395C3F3');
|
||||
$customerMeta->addUniqueIndex(['customer_id', 'name'], 'UNIQ_A48A760F9395C3F35E237E06');
|
||||
$customerMeta->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A48A760F9395C3F3');
|
||||
@@ -56,7 +56,7 @@ final class Version20190617100845 extends AbstractMigration
|
||||
$projectMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$projectMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$projectMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($projectMeta, ['id']);
|
||||
$projectMeta->setPrimaryKey(['id']);
|
||||
$projectMeta->addIndex(['project_id'], 'IDX_50536EF2166D1F9C');
|
||||
$projectMeta->addUniqueIndex(['project_id', 'name'], 'UNIQ_50536EF2166D1F9C5E237E06');
|
||||
$projectMeta->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50536EF2166D1F9C');
|
||||
@@ -67,7 +67,7 @@ final class Version20190617100845 extends AbstractMigration
|
||||
$activityMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$activityMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$activityMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($activityMeta, ['id']);
|
||||
$activityMeta->setPrimaryKey(['id']);
|
||||
$activityMeta->addIndex(['activity_id'], 'IDX_A7C0A43D81C06096');
|
||||
$activityMeta->addUniqueIndex(['activity_id', 'name'], 'UNIQ_A7C0A43D81C060965E237E06');
|
||||
$activityMeta->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A7C0A43D81C06096');
|
||||
|
||||
@@ -32,7 +32,7 @@ final class Version20190730123324 extends AbstractMigration
|
||||
$teams->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$teams->addColumn('name', 'string', ['notnull' => true, 'length' => 100]);
|
||||
$teams->addColumn('teamlead_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$this->addPrimaryKeyConstraint($teams, ['id']);
|
||||
$teams->setPrimaryKey(['id']);
|
||||
$teams->addUniqueIndex(['name'], 'UNIQ_3BEDDC7F5E237E06');
|
||||
$teams->addForeignKeyConstraint('kimai2_users', ['teamlead_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_3BEDDC7F8F7DE5D7');
|
||||
|
||||
@@ -41,21 +41,21 @@ final class Version20190730123324 extends AbstractMigration
|
||||
$userTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$userTeams->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8A76ED395');
|
||||
$userTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8296CD8AE');
|
||||
$this->addPrimaryKeyConstraint($userTeams, ['user_id', 'team_id']);
|
||||
$userTeams->setPrimaryKey(['user_id', 'team_id']);
|
||||
|
||||
$customerTeams = $schema->createTable('kimai2_customers_teams');
|
||||
$customerTeams->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$customerTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$customerTeams->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD83889395C3F3');
|
||||
$customerTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD8388296CD8AE');
|
||||
$this->addPrimaryKeyConstraint($customerTeams, ['customer_id', 'team_id']);
|
||||
$customerTeams->setPrimaryKey(['customer_id', 'team_id']);
|
||||
|
||||
$projectTeams = $schema->createTable('kimai2_projects_teams');
|
||||
$projectTeams->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$projectTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$projectTeams->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431166D1F9C');
|
||||
$projectTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431296CD8AE');
|
||||
$this->addPrimaryKeyConstraint($projectTeams, ['project_id', 'team_id']);
|
||||
$projectTeams->setPrimaryKey(['project_id', 'team_id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
|
||||
@@ -31,7 +31,7 @@ final class Version20191108151534 extends AbstractMigration
|
||||
$roles = $schema->createTable('kimai2_roles');
|
||||
$roles->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$roles->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$this->addPrimaryKeyConstraint($roles, ['id']);
|
||||
$roles->setPrimaryKey(['id']);
|
||||
$roles->addUniqueIndex(['name'], 'roles_name');
|
||||
|
||||
$rolePermissions = $schema->createTable('kimai2_roles_permissions');
|
||||
@@ -39,7 +39,7 @@ final class Version20191108151534 extends AbstractMigration
|
||||
$rolePermissions->addColumn('role_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$rolePermissions->addColumn('permission', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$rolePermissions->addColumn('allowed', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($rolePermissions, ['id']);
|
||||
$rolePermissions->setPrimaryKey(['id']);
|
||||
$rolePermissions->addUniqueIndex(['role_id', 'permission'], 'role_permission');
|
||||
$rolePermissions->addForeignKeyConstraint('kimai2_roles', ['role_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_D263A3B8D60322AC');
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class Version20200109102138 extends AbstractMigration
|
||||
$customerComment->addColumn('created_by_id', 'integer', ['notnull' => true]);
|
||||
$customerComment->addColumn('created_at', 'datetime', ['notnull' => true]);
|
||||
$customerComment->addColumn('pinned', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($customerComment, ['id']);
|
||||
$customerComment->setPrimaryKey(['id']);
|
||||
$customerComment->addIndex(['customer_id'], 'IDX_A5B142D99395C3F3');
|
||||
$customerComment->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A5B142D99395C3F3');
|
||||
$customerComment->addForeignKeyConstraint('kimai2_users', ['created_by_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A5B142D9B03A8386');
|
||||
@@ -47,7 +47,7 @@ final class Version20200109102138 extends AbstractMigration
|
||||
$projectComment->addColumn('created_by_id', 'integer', ['notnull' => true]);
|
||||
$projectComment->addColumn('created_at', 'datetime', ['notnull' => true]);
|
||||
$projectComment->addColumn('pinned', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($projectComment, ['id']);
|
||||
$projectComment->setPrimaryKey(['id']);
|
||||
$projectComment->addIndex(['project_id'], 'IDX_29A23638166D1F9C');
|
||||
$projectComment->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_29A23638166D1F9C');
|
||||
$projectComment->addForeignKeyConstraint('kimai2_users', ['created_by_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_29A23638B03A8386');
|
||||
|
||||
@@ -37,7 +37,7 @@ final class Version20200205115243 extends AbstractMigration
|
||||
$customerRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AECA76ED395');
|
||||
$customerRates->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AEC9395C3F3');
|
||||
$customerRates->addUniqueIndex(['user_id', 'customer_id'], 'UNIQ_82AB0AECA76ED3959395C3F3');
|
||||
$this->addPrimaryKeyConstraint($customerRates, ['id']);
|
||||
$customerRates->setPrimaryKey(['id']);
|
||||
|
||||
$projectRates = $schema->createTable('kimai2_projects_rates');
|
||||
$projectRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
@@ -48,7 +48,7 @@ final class Version20200205115243 extends AbstractMigration
|
||||
$projectRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55A76ED395');
|
||||
$projectRates->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55166D1F9C');
|
||||
$projectRates->addUniqueIndex(['user_id', 'project_id'], 'UNIQ_41535D55A76ED395166D1F9C');
|
||||
$this->addPrimaryKeyConstraint($projectRates, ['id']);
|
||||
$projectRates->setPrimaryKey(['id']);
|
||||
|
||||
$activityRates = $schema->createTable('kimai2_activities_rates');
|
||||
$activityRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
@@ -59,7 +59,7 @@ final class Version20200205115243 extends AbstractMigration
|
||||
$activityRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BEA76ED395');
|
||||
$activityRates->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BE81C06096');
|
||||
$activityRates->addUniqueIndex(['user_id', 'activity_id'], 'UNIQ_4A7F11BEA76ED39581C06096');
|
||||
$this->addPrimaryKeyConstraint($activityRates, ['id']);
|
||||
$activityRates->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
|
||||
@@ -45,7 +45,7 @@ final class Version20200308171950 extends AbstractMigration
|
||||
$invoices->addUniqueIndex(['invoice_filename'], 'UNIQ_76C38E372323B33D');
|
||||
$invoices->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_76C38E37A76ED395');
|
||||
$invoices->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_76C38E379395C3F3');
|
||||
$this->addPrimaryKeyConstraint($invoices, ['id']);
|
||||
$invoices->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
|
||||
@@ -31,7 +31,7 @@ final class Version20200524142042 extends AbstractMigration
|
||||
$sessions->addColumn('data', 'blob', ['length' => 65535, 'notnull' => true]);
|
||||
$sessions->addColumn('time', 'integer', ['unsigned' => true, 'notnull' => true]);
|
||||
$sessions->addColumn('lifetime', 'integer', ['unsigned' => true, 'notnull' => true]);
|
||||
$this->addPrimaryKeyConstraint($sessions, ['id']);
|
||||
$sessions->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
|
||||
@@ -33,7 +33,7 @@ final class Version20200725213424 extends AbstractMigration
|
||||
$activityTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$activityTeams->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_986998DA81C06096');
|
||||
$activityTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_986998DA296CD8AE');
|
||||
$this->addPrimaryKeyConstraint($activityTeams, ['activity_id', 'team_id']);
|
||||
$activityTeams->setPrimaryKey(['activity_id', 'team_id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
|
||||
@@ -36,7 +36,7 @@ final class Version20210316224358 extends AbstractMigration
|
||||
$bookmarks->addColumn('content', 'text', ['notnull' => true]);
|
||||
$bookmarks->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4016EF25A76ED395');
|
||||
$bookmarks->addUniqueIndex(['user_id', 'name'], 'UNIQ_4016EF25A76ED3955E237E06');
|
||||
$this->addPrimaryKeyConstraint($bookmarks, ['id']);
|
||||
$bookmarks->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
|
||||
@@ -30,7 +30,7 @@ final class Version20210802174318 extends AbstractMigration
|
||||
$teamMember->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$teamMember->addColumn('teamlead', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$teamMember->dropPrimaryKey();
|
||||
$this->addPrimaryKeyConstraint($teamMember, ['id']);
|
||||
$teamMember->setPrimaryKey(['id']);
|
||||
$teamMember->addUniqueIndex(['user_id', 'team_id'], 'UNIQ_B5E92CF8A76ED395296CD8AE');
|
||||
}
|
||||
|
||||
@@ -41,6 +41,6 @@ final class Version20210802174318 extends AbstractMigration
|
||||
$teamMember->dropPrimaryKey();
|
||||
$teamMember->dropColumn('teamlead');
|
||||
$teamMember->dropColumn('id');
|
||||
$this->addPrimaryKeyConstraint($teamMember, ['user_id', 'team_id']);
|
||||
$teamMember->setPrimaryKey(['user_id', 'team_id']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ final class Version20220101204501 extends AbstractMigration
|
||||
$table->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$table->addColumn('value', 'text', ['notnull' => false, 'length' => 65535]);
|
||||
$table->addColumn('visible', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$this->addPrimaryKeyConstraint($table, ['id']);
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['invoice_id'], 'IDX_7EDC37D92989F1FD');
|
||||
$table->addUniqueIndex(['invoice_id', 'name'], 'UNIQ_7EDC37D92989F1FD5E237E06');
|
||||
$table->addForeignKeyConstraint('kimai2_invoices', ['invoice_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_7EDC37D92989F1FD');
|
||||
|
||||
@@ -35,7 +35,7 @@ final class Version20230327143628 extends AbstractMigration
|
||||
$workingTimes->addColumn('expected', 'integer', ['notnull' => true]);
|
||||
$workingTimes->addColumn('actual', 'integer', ['notnull' => true]);
|
||||
$workingTimes->addColumn('approved_at', 'datetime', ['notnull' => false, 'default' => null]);
|
||||
$this->addPrimaryKeyConstraint($workingTimes, ['id']);
|
||||
$workingTimes->setPrimaryKey(['id']);
|
||||
$workingTimes->addIndex(['user_id'], 'IDX_F95E4933A76ED395');
|
||||
$workingTimes->addIndex(['approved_by'], 'IDX_F95E49334EA3CB3D');
|
||||
$workingTimes->addUniqueIndex(['user_id', 'date'], 'UNIQ_F95E4933A76ED395AA9E377A');
|
||||
|
||||
@@ -33,7 +33,7 @@ final class Version20240214061246 extends AbstractMigration
|
||||
$accessTokens->addColumn('last_usage', 'datetime_immutable', ['notnull' => false, 'default' => null]);
|
||||
$accessTokens->addColumn('expires_at', 'datetime_immutable', ['notnull' => false, 'default' => null]);
|
||||
|
||||
$this->addPrimaryKeyConstraint($accessTokens, ['id']);
|
||||
$accessTokens->setPrimaryKey(['id']);
|
||||
|
||||
$accessTokens->addIndex(['user_id'], 'IDX_6FB0DB1EA76ED395');
|
||||
$accessTokens->addUniqueIndex(['token'], 'UNIQ_6FB0DB1E5F37A13B');
|
||||
|
||||
@@ -33,7 +33,7 @@ final class Version20250608143244 extends AbstractMigration
|
||||
$table->addColumn('columns', 'json', ['notnull' => true]);
|
||||
$table->addColumn('options', 'json', ['notnull' => true]);
|
||||
|
||||
$this->addPrimaryKeyConstraint($table, ['id']);
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addUniqueIndex(['title'], 'UNIQ_2F0CA26F2B36786B');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,109 +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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
/**
|
||||
* @version 3.0
|
||||
*/
|
||||
final class Version20260101010000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Kimai 3.0 update';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$connection = $this->connection;
|
||||
|
||||
$rows = $connection->fetchAllAssociative('SELECT id, roles FROM kimai2_users');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$id = $row['id'];
|
||||
$value = $row['roles'];
|
||||
$data = [];
|
||||
|
||||
if (\is_string($value) && str_starts_with($value, 'a:')) {
|
||||
$data = unserialize($value);
|
||||
}
|
||||
|
||||
if (!\is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$json = json_encode($data);
|
||||
|
||||
$connection->executeStatement('UPDATE kimai2_users SET roles = :data WHERE id = :id', [
|
||||
'data' => $json,
|
||||
'id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
$table = $schema->getTable('kimai2_users');
|
||||
$table->dropColumn('api_token');
|
||||
|
||||
$column = $table->getColumn('roles');
|
||||
$column->setType(Type::getType(Types::JSON));
|
||||
$column->setComment('(DC2Type:json)');
|
||||
$column->setNotnull(true);
|
||||
|
||||
$table = $schema->getTable('kimai2_timesheet');
|
||||
$table->dropColumn('category');
|
||||
|
||||
$this->addSql("DELETE FROM kimai2_user_preferences WHERE `name` = 'export_decimal'");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$connection = $this->connection;
|
||||
|
||||
// this will leave behind a json_valid() check in mariadb
|
||||
$connection->executeStatement('ALTER TABLE kimai2_users CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\'');
|
||||
|
||||
// Fetch the existing rows from the table
|
||||
$rows = $connection->fetchAllAssociative('SELECT id, roles FROM kimai2_users');
|
||||
|
||||
// Iterate over each row
|
||||
foreach ($rows as $row) {
|
||||
$id = $row['id'];
|
||||
$roles = $row['roles'];
|
||||
$data = [];
|
||||
|
||||
if (json_validate($roles)) {
|
||||
$data = json_decode($roles, true);
|
||||
}
|
||||
|
||||
if (!\is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$connection->executeStatement('UPDATE kimai2_users SET roles = :roles WHERE id = :id', [
|
||||
'roles' => serialize($data),
|
||||
'id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
$timesheetTable = $schema->getTable('kimai2_timesheet');
|
||||
$timesheetTable->addColumn('category', 'string', ['length' => 10, 'notnull' => true, 'default' => 'work']);
|
||||
|
||||
$usersTable = $schema->getTable('kimai2_users');
|
||||
$usersTable->addColumn('api_token', 'string', ['length' => 255, 'notnull' => false, 'default' => null]);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
113
package.json
113
package.json
@@ -1,59 +1,58 @@
|
||||
{
|
||||
"name": "kimai",
|
||||
"homepage": "https://github.com/kimai/kimai",
|
||||
"license": "AGPL",
|
||||
"private": true,
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Kevin Papst",
|
||||
"url": "https://www.kevinpapst.de"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/kimai/kimai.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev-server": "yarn encore dev-server",
|
||||
"dev": "yarn encore dev",
|
||||
"watch": "yarn encore dev --watch",
|
||||
"lint": "yarn eslint assets/js/",
|
||||
"build": "yarn encore production"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/core": "^7",
|
||||
"@babel/eslint-parser": "^7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7",
|
||||
"@babel/preset-env": "^7",
|
||||
"@eslint/js": "^9",
|
||||
"@fortawesome/fontawesome-free": "^6",
|
||||
"@fullcalendar/bootstrap5": "^5",
|
||||
"@fullcalendar/core": "^5",
|
||||
"@fullcalendar/daygrid": "^5",
|
||||
"@fullcalendar/google-calendar": "^5",
|
||||
"@fullcalendar/icalendar": "^5",
|
||||
"@fullcalendar/interaction": "^5",
|
||||
"@fullcalendar/timegrid": "^5",
|
||||
"@popperjs/core": "^2",
|
||||
"@symfony/webpack-encore": "^5",
|
||||
"@tabler/core": "1.4",
|
||||
"bootstrap": "^5.3",
|
||||
"chart.js": "^4",
|
||||
"core-js": "^3",
|
||||
"eslint": "^9",
|
||||
"globals": "^15",
|
||||
"gridstack": "^7",
|
||||
"highlight.js": "^11",
|
||||
"litepicker": "^2",
|
||||
"luxon": "^3",
|
||||
"sass": "^1",
|
||||
"sass-loader": "^16",
|
||||
"tom-select": "^2",
|
||||
"webpack": "^5",
|
||||
"webpack-cli": "^5"
|
||||
},
|
||||
"packageManager": "yarn@4.5.3"
|
||||
"name": "kimai",
|
||||
"homepage": "https://github.com/kimai/kimai",
|
||||
"license": "AGPL",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Kevin Papst",
|
||||
"url": "https://www.kevinpapst.de"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/kimai/kimai.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev-server": "yarn encore dev-server",
|
||||
"dev": "yarn encore dev",
|
||||
"watch": "yarn encore dev --watch",
|
||||
"lint": "yarn eslint assets/js/",
|
||||
"build": "yarn encore production"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/core": "^7",
|
||||
"@babel/eslint-parser": "^7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7",
|
||||
"@babel/preset-env": "^7",
|
||||
"@eslint/js": "^9",
|
||||
"@fortawesome/fontawesome-free": "^6",
|
||||
"@fullcalendar/bootstrap5": "^5",
|
||||
"@fullcalendar/core": "^5",
|
||||
"@fullcalendar/daygrid": "^5",
|
||||
"@fullcalendar/google-calendar": "^5",
|
||||
"@fullcalendar/icalendar": "^5",
|
||||
"@fullcalendar/interaction": "^5",
|
||||
"@fullcalendar/timegrid": "^5",
|
||||
"@popperjs/core": "^2",
|
||||
"@symfony/webpack-encore": "^5",
|
||||
"@tabler/core": "1.4",
|
||||
"bootstrap": "^5.3",
|
||||
"chart.js": "^4",
|
||||
"core-js": "^3",
|
||||
"eslint": "^9",
|
||||
"globals": "^15",
|
||||
"gridstack": "^7",
|
||||
"highlight.js": "^11.11.1",
|
||||
"litepicker": "^2",
|
||||
"luxon": "^3",
|
||||
"sass": "^1",
|
||||
"sass-loader": "^16",
|
||||
"tom-select": "^2.4.3",
|
||||
"webpack": "^5",
|
||||
"webpack-cli": "^5"
|
||||
},
|
||||
"packageManager": "yarn@4.5.3"
|
||||
}
|
||||
|
||||
199
phpstan.neon
199
phpstan.neon
@@ -27,6 +27,7 @@ parameters:
|
||||
numericOperandsInArithmeticOperators: true
|
||||
switchConditionsMatchingType: true
|
||||
noVariableVariables: false
|
||||
reportNonIntStringArrayKey: false
|
||||
paths:
|
||||
- src
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
@@ -49,6 +50,16 @@ parameters:
|
||||
count: 1
|
||||
path: src/API/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\API\\\\Authentication\\\\TokenAuthenticator\\:\\:getCredentials\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/API/Authentication/TokenAuthenticator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\PasswordHasherInterface\\:\\:verify\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/API/Authentication/TokenAuthenticator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of method App\\\\Entity\\\\Customer\\:\\:getMetaField\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -169,6 +180,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Command/AbstractUserCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/AbstractUserCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$username of method App\\\\User\\\\UserService\\:\\:findUserByUsernameOrThrowException\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -389,6 +405,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Command/PromoteUserCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/RegenerateLocalesCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method find\\(\\) on Symfony\\\\Component\\\\Console\\\\Application\\|null\\.$#"
|
||||
count: 4
|
||||
@@ -574,6 +595,21 @@ parameters:
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:offsetExists\\(\\) has parameter \\$offset with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:offsetGet\\(\\) has parameter \\$offset with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$key of method App\\\\Configuration\\\\SystemConfiguration\\:\\:set\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function trim expects string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
@@ -1139,11 +1175,21 @@ parameters:
|
||||
count: 1
|
||||
path: src/Entity/Team.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\Timesheet\\:\\:getCategory\\(\\) should return string but returns string\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Entity/Timesheet.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\Timesheet\\:\\:getMetaFields\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Entity/Timesheet.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\Timesheet\\:\\:getTags\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Entity/Timesheet.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of method App\\\\Entity\\\\Timesheet\\:\\:getMetaField\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -1219,6 +1265,21 @@ parameters:
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:setConfirmationToken\\(\\) has parameter \\$confirmationToken with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:setPassword\\(\\) has parameter \\$password with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:setPlainPassword\\(\\) has parameter \\$password with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:setRoles\\(\\) has parameter \\$roles with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -1304,6 +1365,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Event/RevenueStatisticEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\ThemeJavascriptTranslationsEvent\\:\\:getTranslations\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Event/ThemeJavascriptTranslationsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#"
|
||||
count: 2
|
||||
@@ -1364,6 +1430,16 @@ parameters:
|
||||
count: 1
|
||||
path: src/Export/Annotation/Order.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Export\\\\Base\\\\HtmlRenderer\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Export/Base/HtmlRenderer.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Export\\\\Base\\\\PDFRenderer\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Export/Base/PDFRenderer.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Export\\\\Base\\\\PDFRenderer\\:\\:getPdfOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -1864,6 +1940,61 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/TimesheetPreCreateForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addActivityMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addActivitySelect\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addCustomerMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addCustomerSelect\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addDateRange\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addOrderBy\\(\\) has parameter \\$allowedColumns with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addProjectMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addProjectSelect\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addTeamsChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\AbstractToolbarForm\\:\\:addUsersChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method App\\\\Repository\\\\Query\\\\BaseFormTypeQuery\\:\\:setUser\\(\\) expects App\\\\Entity\\\\User, mixed given\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Toolbar/AbstractToolbarForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Toolbar\\\\ActivityToolbarForm\\:\\:addActivityMultiChoice\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -3191,7 +3322,7 @@ parameters:
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$identifier of method App\\\\Entity\\\\User\\:\\:setUserIdentifier\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
count: 2
|
||||
path: src/Saml/SamlProvider.php
|
||||
|
||||
-
|
||||
@@ -3199,6 +3330,16 @@ parameters:
|
||||
count: 1
|
||||
path: src/Saml/SamlProvider.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Saml\\\\Security\\\\SamlAuthenticationFailureHandler\\:\\:\\$defaultOptions has no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Saml/Security/SamlAuthenticationFailureHandler.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Saml\\\\Security\\\\SamlAuthenticationSuccessHandler\\:\\:\\$defaultOptions has no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Saml/Security/SamlAuthenticationSuccessHandler.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Security\\\\KimaiUserProvider\\:\\:getProviders\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -3335,24 +3476,34 @@ parameters:
|
||||
path: src/Twig/Context.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:checkInColumDefinition\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:checkInColumDefinition\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:getDatatableColumnClass\\(\\) should return string but returns bool\\|string\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:getClass\\(\\) has parameter \\$class with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) has parameter \\$defaultColumns with no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:getDatatableColumnClass\\(\\) should return string but returns bool\\|string\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) has parameter \\$defaultColumns with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Twig\\\\DatatableExtensions\\:\\:\\$tableNames type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Extensions\\:\\:replaceNewline\\(\\) has no return type specified\\.$#"
|
||||
@@ -3389,6 +3540,21 @@ parameters:
|
||||
count: 1
|
||||
path: src/Twig/LocaleFormatExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\PaginationExtension\\:\\:createRouteGenerator\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/PaginationExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\PaginationExtension\\:\\:renderPagination\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/PaginationExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\PaginationExtension\\:\\:renderPagination\\(\\) has parameter \\$pager with generic class Pagerfanta\\\\Pagerfanta but does not specify its types\\: T$#"
|
||||
count: 1
|
||||
path: src/Twig/PaginationExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\ThemeExtension\\:\\:actions\\(\\) has parameter \\$payload with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -3429,6 +3595,11 @@ parameters:
|
||||
count: 3
|
||||
path: src/Utils/Color.php
|
||||
|
||||
-
|
||||
message: "#^Class App\\\\Utils\\\\DataTable implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
|
||||
count: 1
|
||||
path: src/Utils/DataTable.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Utils\\\\DataTable\\:\\:addColumn\\(\\) has parameter \\$column with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -3439,6 +3610,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Utils/DataTable.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Utils\\\\DataTable\\:\\:getIterator\\(\\) should return Traversable\\<mixed, mixed\\> but returns Traversable\\<\\(int\\|string\\), mixed\\>\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/DataTable.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Utils\\\\DataTable\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -3579,6 +3755,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Utils/PageSetup.php
|
||||
|
||||
-
|
||||
message: "#^Class App\\\\Utils\\\\Pagination extends generic class Pagerfanta\\\\Pagerfanta but does not specify its types\\: T$#"
|
||||
count: 1
|
||||
path: src/Utils/Pagination.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$class of method App\\\\Utils\\\\PaginationTemplate\\:\\:linkLi\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
|
||||
139
phpunit.xml.dist
139
phpunit.xml.dist
@@ -1,12 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
cacheDirectory="var/cache/phpunit/"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
@@ -14,86 +10,59 @@
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
displayDetailsOnPhpunitDeprecations="true">
|
||||
<php>
|
||||
<ini name="display_errors" value="1" />
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<ini name="max_execution_time" value="-1"/>
|
||||
<ini name="date.timezone" value="UTC"/>
|
||||
<ini name="intl.default_locale" value="en"/>
|
||||
<ini name="date.timezone" value="Europe/Vienna"/>
|
||||
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
<env name="APP_DEBUG" value="0" force="true"/>
|
||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?charset=utf8mb4&serverVersion=10.11.15-MariaDB"/>
|
||||
<!-- ###+ nelmio/cors-bundle ### -->
|
||||
<env name="CORS_ALLOW_ORIGIN" value="'^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'"/>
|
||||
<!-- ###- nelmio/cors-bundle ### -->
|
||||
<!--
|
||||
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<ini name="max_execution_time" value="-1"/>
|
||||
<ini name="date.timezone" value="UTC"/>
|
||||
<ini name="intl.default_locale" value="en"/>
|
||||
<ini name="date.timezone" value="Europe/Vienna"/>
|
||||
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||
<env name="APP_ENV" value="test" force="true"/>
|
||||
<env name="APP_DEBUG" value="0" force="true"/>
|
||||
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?charset=utf8mb4&serverVersion=10.5.8-MariaDB"/>
|
||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||
<env name="MAILER_URL" value="null://null"/>
|
||||
<env name="MAILER_FROM" value="kimai@example.com"/>
|
||||
<!--
|
||||
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
|
||||
|
||||
- change to "true"
|
||||
- composer tests
|
||||
- change back to "false"
|
||||
- change to "true"
|
||||
- composer tests
|
||||
- change back to "false"
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
|
||||
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
|
||||
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
|
||||
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
||||
-->
|
||||
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
||||
|
||||
<!-- ###+ symfony/framework-bundle ### -->
|
||||
<env name="APP_ENV" value="test" force="true"/>
|
||||
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
||||
<env name="APP_SHARE_DIR" value="var/share"/>
|
||||
<!-- ###- symfony/framework-bundle ### -->
|
||||
<!-- ###+ symfony/mailer ### -->
|
||||
<env name="MAILER_URL" value="null://null"/>
|
||||
<env name="MAILER_FROM" value="kimai@example.com"/>
|
||||
<!-- ###- symfony/mailer ### -->
|
||||
|
||||
<!-- ###+ symfony/routing ### -->
|
||||
<!-- Configure how to generate URLs in non-HTTP contexts, such as CLI commands. -->
|
||||
<!-- See https://symfony.com/doc/current/routing.html#generating-urls-in-commands -->
|
||||
<env name="DEFAULT_URI" value="http://localhost"/>
|
||||
<!-- ###- symfony/routing ### -->
|
||||
|
||||
<!-- ###+ symfony/messenger ### -->
|
||||
<!-- Choose one of the transports below -->
|
||||
<!-- MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages -->
|
||||
<!-- MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages -->
|
||||
<env name="MESSENGER_TRANSPORT_DSN" value="doctrine://default?auto_setup=0"/>
|
||||
<!-- ###- symfony/messenger ### -->
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="Kimai">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<extensions>
|
||||
<bootstrap class="Symfony\Bridge\PhpUnit\SymfonyExtension">
|
||||
<parameter name="clock-mock-namespaces" value="App" />
|
||||
<parameter name="dns-mock-namespaces" value="App" />
|
||||
</bootstrap>
|
||||
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
|
||||
</extensions>
|
||||
<source ignoreSuppressionOfDeprecations="true" restrictNotices="true" restrictWarnings="true">
|
||||
<include>
|
||||
<directory suffix=".php">src/</directory>
|
||||
<directory suffix=".php">templates/</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory suffix=".php">migrations/</directory>
|
||||
<directory suffix=".php">assets/</directory>
|
||||
<directory suffix=".php">bin/</directory>
|
||||
<directory suffix=".php">config/</directory>
|
||||
<directory suffix=".php">node_modules/</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>
|
||||
</source>
|
||||
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
|
||||
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
|
||||
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
|
||||
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
||||
-->
|
||||
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="Kimai">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<extensions>
|
||||
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
|
||||
</extensions>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src/</directory>
|
||||
<directory suffix=".php">templates/</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory suffix=".php">migrations/</directory>
|
||||
<directory suffix=".php">assets/</directory>
|
||||
<directory suffix=".php">bin/</directory>
|
||||
<directory suffix=".php">config/</directory>
|
||||
<directory suffix=".php">node_modules/</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>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/app.41748d55.js
Normal file
2
public/build/app.41748d55.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/calendar.36a92145.js
Normal file
2
public/build/calendar.36a92145.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/chart.56f16a68.js
Normal file
2
public/build/chart.56f16a68.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* @kurkle/color v0.3.2
|
||||
* @kurkle/color v0.3.4
|
||||
* https://github.com/kurkle/color#readme
|
||||
* (c) 2023 Jukka Kurkela
|
||||
* (c) 2024 Jukka Kurkela
|
||||
* Released under the MIT License
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/dashboard.9708ae5e.js
Normal file
2
public/build/dashboard.9708ae5e.js
Normal file
File diff suppressed because one or more lines are too long
@@ -2,25 +2,25 @@
|
||||
"entrypoints": {
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app.3ba0bd00.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/app.41748d55.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.16ea604e.css"
|
||||
"/build/app.6bfb9c66.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/app-rtl.15853b82.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.f149299b.css"
|
||||
"/build/app-rtl.16271884.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/export-pdf.395749ab.js"
|
||||
],
|
||||
"css": [
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"invoice": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/invoice.42b319e4.js"
|
||||
],
|
||||
"css": [
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"invoice-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/invoice-pdf.26d98626.js"
|
||||
],
|
||||
"css": [
|
||||
@@ -47,14 +47,14 @@
|
||||
},
|
||||
"chart": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/chart.bafa38e7.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/chart.56f16a68.js"
|
||||
]
|
||||
},
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/calendar.4242f779.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/calendar.36a92145.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.d757753e.css"
|
||||
@@ -62,8 +62,8 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/dashboard.6ce7ac9c.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/dashboard.9708ae5e.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/dashboard.b7129fa1.css"
|
||||
@@ -71,8 +71,8 @@
|
||||
},
|
||||
"highlight": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/highlight.13d5d50e.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/highlight.0296a734.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/highlight.98bf3927.css"
|
||||
@@ -80,23 +80,23 @@
|
||||
}
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
||||
"/build/app.3ba0bd00.js": "sha384-4G73IK+GVsttYdvW0Pt2Hov27rTJuGDusRBvoRnzPezDlTiArF4MV9mVDi7anlX+",
|
||||
"/build/app.16ea604e.css": "sha384-9734FGgqgVw+kNVry5jl15+pbaetOtfl3eywAy2pSigGpeIXlCh1udhh7rI9lNKo",
|
||||
"/build/runtime.684e9f6d.js": "sha384-suKiEX2de4fdNqQzdYbUd6osp4AepD9FiMXl+1QdvgMW9dcQqUWQNQasf3KWzwLr",
|
||||
"/build/app.41748d55.js": "sha384-iVPHnOXYjVDuq8567h8I3U2gl5SCV3mNMFNQCJdgMiNuFMZ4KO7PXOLanjgcGzTt",
|
||||
"/build/app.6bfb9c66.css": "sha384-38+zSolYBrZdSwNcrDi5qCaQGCQlBJRKwtsJV+pnrf8rCCpf6z/M7fEpV4tS79Sz",
|
||||
"/build/app-rtl.15853b82.js": "sha384-UnKKgLMu9FnRT+CFE0no/+UiUks012bYriQdUWa6f02mo6Lswl947mPybjvKL503",
|
||||
"/build/app-rtl.f149299b.css": "sha384-nCTYbFHUkkF20RA5oXqGah3iDnnSUxAmuFHpbehTzHszCgJBVRoHC81Q5OwFreLf",
|
||||
"/build/app-rtl.16271884.css": "sha384-P1x4vYfjfM4JL55NQ3sfLiy+BSy8cM4bNcshj9M62nMJ4JD2xAWUioNv6VrmaK7U",
|
||||
"/build/export-pdf.395749ab.js": "sha384-3Hjvmu4FC/0dhHnR8kyRBU7k2xMNy1lxBpGgOkrw8PxXnwyQDM8/5bQmkJbjVT1+",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.42b319e4.js": "sha384-xxK7sCe/ZhTjMPFPeX1xvILURxNRZz2hJHZxAGVaw9zE6TC++2/6y2eKqg8cz852",
|
||||
"/build/invoice.36018785.css": "sha384-jukM9uZ6pexDxXKgZThSxiqXimzsxzniBMHz08N9x8ryXZYkM5r/ZgaainCV0+J6",
|
||||
"/build/invoice-pdf.26d98626.js": "sha384-gwNzQiU1y6qU/M9DPGiNW0MVZkLctEHk37sCES2X9ov+zugEaDABdkMjKBYOC9lz",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.bafa38e7.js": "sha384-Ays2qGKvOqs4NSeN/zJOPcrnzIEC/uYSCIkC3kN0KQye+mA5Lq7UkfjZDjulvFUX",
|
||||
"/build/calendar.4242f779.js": "sha384-fz1ouLGHV92+3gq3eWYAgEIbwVSmuCfILeVjAnnXGjJJfUtMs+qlGEIIo3uQa0rz",
|
||||
"/build/chart.56f16a68.js": "sha384-SWnYjAbZ8OWEvTP+IZfGuBMWJIcH9OZWLYQ4p38KMPmqfufsc2zhzqWkALO3mCBO",
|
||||
"/build/calendar.36a92145.js": "sha384-ZPLxxbep6OnPeUiIeBXRE1iscZFgbNuTpn3rx2Jpf1KR2l6sxIuNsieDUskbmQ3m",
|
||||
"/build/calendar.d757753e.css": "sha384-cTmQMgHYjd2gfObFWmEUph7qQLCyXaIkneSf+bQ2mqVmZwqOB+pJOm/UYTyTjALJ",
|
||||
"/build/dashboard.6ce7ac9c.js": "sha384-5BOoyjZx/ZKr9IbogWfT/aBRHx7XA1fePADdUpUNlSF/rqVo3taXLLcrIoaDZ+8b",
|
||||
"/build/dashboard.9708ae5e.js": "sha384-QN7XIQuxFZu76sHVrgdpZL81+Q2VTwcgF3aI2CnpwYZiMoPbzrcTGfPL3RDy696t",
|
||||
"/build/dashboard.b7129fa1.css": "sha384-2nn5hLA+3YedgHYBpge62S8Losj8aoPwK9Zk9EvN1xEYatvOUQ7H3rIR2UUJAGOS",
|
||||
"/build/highlight.13d5d50e.js": "sha384-+RqBBPIzp5YQpbBeHgVNXzHiewK1PZtizPh4in1PlBU6RcT1bDleW3QSJMDeBnPO",
|
||||
"/build/highlight.0296a734.js": "sha384-vGFKI/KM+uyGIlzKsKhQh4w41hQteB4u14oRYqObMT03/YTZXq6Re5nUN6g3sGQH",
|
||||
"/build/highlight.98bf3927.css": "sha384-YgweSwDwN0dI4DEmh478xYVw/TewJYvCO1QTbWHpaHFDPuLrZvYMR0Tc+QfFxVPE"
|
||||
}
|
||||
}
|
||||
1
public/build/highlight.0296a734.js
Normal file
1
public/build/highlight.0296a734.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build/app.css": "/build/app.16ea604e.css",
|
||||
"build/app.js": "/build/app.3ba0bd00.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.f149299b.css",
|
||||
"build/app.css": "/build/app.6bfb9c66.css",
|
||||
"build/app.js": "/build/app.41748d55.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.16271884.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.15853b82.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.395749ab.js",
|
||||
@@ -9,14 +9,14 @@
|
||||
"build/invoice.js": "/build/invoice.42b319e4.js",
|
||||
"build/invoice-pdf.css": "/build/invoice-pdf.2b749265.css",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.26d98626.js",
|
||||
"build/chart.js": "/build/chart.bafa38e7.js",
|
||||
"build/chart.js": "/build/chart.56f16a68.js",
|
||||
"build/calendar.css": "/build/calendar.d757753e.css",
|
||||
"build/calendar.js": "/build/calendar.4242f779.js",
|
||||
"build/calendar.js": "/build/calendar.36a92145.js",
|
||||
"build/dashboard.css": "/build/dashboard.b7129fa1.css",
|
||||
"build/dashboard.js": "/build/dashboard.6ce7ac9c.js",
|
||||
"build/dashboard.js": "/build/dashboard.9708ae5e.js",
|
||||
"build/highlight.css": "/build/highlight.98bf3927.css",
|
||||
"build/highlight.js": "/build/highlight.13d5d50e.js",
|
||||
"build/runtime.js": "/build/runtime.6c399d29.js",
|
||||
"build/highlight.js": "/build/highlight.0296a734.js",
|
||||
"build/runtime.js": "/build/runtime.684e9f6d.js",
|
||||
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.2582b0e4.ttf",
|
||||
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.1815e004.ttf",
|
||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.2463b90d.woff2",
|
||||
|
||||
1
public/build/runtime.684e9f6d.js
Normal file
1
public/build/runtime.684e9f6d.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every(function(e){return t.O[e](n[a])})?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={121:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some(function(r){return 0!==e[r]})){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai=self.webpackChunkkimai||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();
|
||||
@@ -1 +0,0 @@
|
||||
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every((function(e){return t.O[e](n[a])}))?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={121:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some((function(r){return 0!==e[r]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai=self.webpackChunkkimai||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();
|
||||
@@ -35,9 +35,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'Activity')]
|
||||
final class ActivityController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Activity', 'Activity_Entity'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Activity'];
|
||||
private const array GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Activity', 'Activity_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Activity'];
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -26,12 +26,22 @@ final class ApiRequestMatcher implements RequestMatcherInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// the next two checks are primarily here to make sure to return proper error messages
|
||||
|
||||
// let's use this firewall if a Bearer token is set in the header
|
||||
// other cases like "bearer" are rejected earlier
|
||||
if (($auth = $request->headers->get('Authorization')) !== null && str_starts_with($auth, 'Bearer ')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// let's use this firewall if the deprecated username & token combination is available
|
||||
if ($request->headers->has(TokenAuthenticator::HEADER_USERNAME) &&
|
||||
$request->headers->has(TokenAuthenticator::HEADER_TOKEN)) {
|
||||
return true;
|
||||
}
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
// checking for a previous session allows us to skip the API firewall and token access handler
|
||||
// we simply re-use the existing session when doing API calls from the frontend.
|
||||
// it is not necessary to check headers. if there is no valid session, we should always use this firewall
|
||||
|
||||
59
src/API/Authentication/ApiTokenMigratingListener.php
Normal file
59
src/API/Authentication/ApiTokenMigratingListener.php
Normal 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\API\Authentication;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
|
||||
final class ApiTokenMigratingListener implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private PasswordHasherFactoryInterface $hasherFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public function onLoginSuccess(LoginSuccessEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport->hasBadge(ApiTokenUpgradeBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ApiTokenUpgradeBadge $badge */
|
||||
$badge = $passport->getBadge(ApiTokenUpgradeBadge::class);
|
||||
$plaintextApiToken = $badge->getAndErasePlaintextApiToken();
|
||||
|
||||
if ('' === $plaintextApiToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $passport->getUser();
|
||||
if (!($user instanceof User)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $user->getApiToken()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$passwordHasher = $this->hasherFactory->getPasswordHasher($user);
|
||||
if (!$passwordHasher->needsRehash($user->getApiToken())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badge->getPasswordUpgrader()->upgradePassword($user, $passwordHasher->hash($plaintextApiToken));
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [LoginSuccessEvent::class => 'onLoginSuccess'];
|
||||
}
|
||||
}
|
||||
43
src/API/Authentication/ApiTokenUpgradeBadge.php
Normal file
43
src/API/Authentication/ApiTokenUpgradeBadge.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\API\Authentication;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
final class ApiTokenUpgradeBadge implements BadgeInterface
|
||||
{
|
||||
public function __construct(private ?string $plaintextApiToken, private readonly PasswordUpgraderInterface $passwordUpgrader)
|
||||
{
|
||||
}
|
||||
|
||||
public function getAndErasePlaintextApiToken(): string
|
||||
{
|
||||
$password = $this->plaintextApiToken;
|
||||
if (null === $password) {
|
||||
throw new LogicException('The api token is erased as another listener already used this badge.');
|
||||
}
|
||||
|
||||
$this->plaintextApiToken = null;
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
public function getPasswordUpgrader(): PasswordUpgraderInterface
|
||||
{
|
||||
return $this->passwordUpgrader;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
117
src/API/Authentication/TokenAuthenticator.php
Normal file
117
src/API/Authentication/TokenAuthenticator.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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\API\Authentication;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\ApiUserRepository;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
|
||||
final class TokenAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public const HEADER_USERNAME = 'X-AUTH-USER';
|
||||
public const HEADER_TOKEN = 'X-AUTH-TOKEN';
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiUserRepository $userProvider,
|
||||
private readonly PasswordHasherFactoryInterface $passwordHasherFactory
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||
if (str_contains($request->getRequestUri(), '/api/doc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->headers->has(self::HEADER_USERNAME) && $request->headers->has(self::HEADER_TOKEN)) {
|
||||
@trigger_error('You are using deprecated API access, please upgrade your APP to use API tokens instead.', E_USER_DEPRECATED);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getCredentials(Request $request): array
|
||||
{
|
||||
$apiUser = $request->headers->get(self::HEADER_USERNAME);
|
||||
if (null === $apiUser || '' === $apiUser) {
|
||||
throw new CustomUserMessageAuthenticationException('Authentication required, missing user header: ' . self::HEADER_USERNAME);
|
||||
}
|
||||
|
||||
$apiToken = $request->headers->get(self::HEADER_TOKEN);
|
||||
if (null === $apiToken || '' === $apiToken) {
|
||||
throw new CustomUserMessageAuthenticationException('Authentication required, missing token header: ' . self::HEADER_TOKEN);
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $apiUser,
|
||||
'password' => $apiToken
|
||||
];
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$credentials = $this->getCredentials($request);
|
||||
|
||||
$checkCredentials = function (?string $presentedPassword, User $user) {
|
||||
if ('' === $presentedPassword) {
|
||||
throw new BadCredentialsException('The presented password cannot be empty.');
|
||||
}
|
||||
|
||||
if (null === $user->getApiToken()) {
|
||||
throw new BadCredentialsException('The user has no activated API account.');
|
||||
}
|
||||
|
||||
if ($this->passwordHasherFactory->getPasswordHasher($user)->verify($user->getApiToken(), $presentedPassword)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
};
|
||||
|
||||
$passport = new Passport(
|
||||
new UserBadge($credentials['username'], [$this->userProvider, 'loadUserByIdentifier']),
|
||||
new CustomCredentials($checkCredentials, $credentials['password'])
|
||||
);
|
||||
|
||||
$passport->addBadge(new ApiTokenUpgradeBadge($credentials['password'], $this->userProvider));
|
||||
|
||||
return $passport;
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
$data = [
|
||||
'message' => $exception instanceof CustomUserMessageAuthenticationException ? $exception->getMessage() : 'Invalid credentials'
|
||||
];
|
||||
|
||||
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'Customer')]
|
||||
final class CustomerController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Customer', 'Customer_Entity'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Customer'];
|
||||
private const array GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Customer', 'Customer_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Customer'];
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -28,8 +28,8 @@ use Symfony\Component\Validator\Constraints;
|
||||
#[OA\Tag(name: 'Invoice')]
|
||||
final class InvoiceController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -37,9 +37,9 @@ use Symfony\Component\Validator\Constraints;
|
||||
#[OA\Tag(name: 'Project')]
|
||||
final class ProjectController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Project', 'Project_Entity'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Project'];
|
||||
private const array GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Project', 'Project_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Project'];
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -24,40 +24,44 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'Default')]
|
||||
final class StatusController extends BaseApiController
|
||||
{
|
||||
public function __construct(private readonly ViewHandlerInterface $viewHandler)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing route for the API
|
||||
*/
|
||||
#[OA\Response(response: 200, description: "A simple route that returns a 'pong', which you can use for testing the API", content: new OA\JsonContent(example: "{'message': 'pong'}"))]
|
||||
#[Route(path: '/ping', methods: ['GET'])]
|
||||
public function pingAction(ViewHandlerInterface $viewHandler): Response
|
||||
#[Route(methods: ['GET'], path: '/ping')]
|
||||
public function pingAction(): Response
|
||||
{
|
||||
$view = new View(['message' => 'pong'], 200);
|
||||
|
||||
return $viewHandler->handle($view);
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Kimai release
|
||||
*/
|
||||
#[OA\Response(response: 200, description: 'Returns version information about the current release', content: new OA\JsonContent(ref: new Model(type: Version::class)))]
|
||||
#[Route(path: '/version', methods: ['GET'])]
|
||||
public function versionAction(ViewHandlerInterface $viewHandler): Response
|
||||
#[Route(methods: ['GET'], path: '/version')]
|
||||
public function versionAction(): Response
|
||||
{
|
||||
return $viewHandler->handle(new View(new Version(), 200));
|
||||
return $this->viewHandler->handle(new View(new Version(), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch installed Plugins
|
||||
*/
|
||||
#[OA\Response(response: 200, description: 'Returns a list of plugin names and versions', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: new Model(type: Plugin::class))))]
|
||||
#[Route(path: '/plugins', methods: ['GET'])]
|
||||
public function pluginAction(PluginManager $pluginManager, ViewHandlerInterface $viewHandler): Response
|
||||
#[Route(methods: ['GET'], path: '/plugins')]
|
||||
public function pluginAction(PluginManager $pluginManager): Response
|
||||
{
|
||||
$plugins = [];
|
||||
foreach ($pluginManager->getPlugins() as $plugin) {
|
||||
$plugins[] = new Plugin($plugin);
|
||||
}
|
||||
|
||||
return $viewHandler->handle(new View($plugins, 200));
|
||||
return $this->viewHandler->handle(new View($plugins, 200));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'Tag')]
|
||||
final class TagController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Tag'];
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Tag'];
|
||||
private const array GROUPS_FORM = ['Default', 'Entity', 'Tag'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Tag'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Tag'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'Tag'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
@@ -39,9 +39,9 @@ final class TagController extends BaseApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch tags as strings
|
||||
* Deprecated: Fetch tags as strings
|
||||
*/
|
||||
#[OA\Response(response: 200, description: 'Returns existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))]
|
||||
#[OA\Response(response: 200, description: 'DEPRECATED: Returns existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))]
|
||||
#[OA\Get(x: ['internal' => true])]
|
||||
#[Route(methods: ['GET'], name: 'get_tags')]
|
||||
#[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')]
|
||||
|
||||
@@ -36,9 +36,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'Team')]
|
||||
final class TeamController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
|
||||
private const array GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Team'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Team'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -47,11 +47,11 @@ use Symfony\Component\Validator\Constraints;
|
||||
#[OA\Tag(name: 'Timesheet')]
|
||||
final class TimesheetController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Not_Expanded'];
|
||||
private const array GROUPS_ENTITY_FULL = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Expanded'];
|
||||
private const array GROUPS_FORM = ['Default', 'Entity', 'Timesheet', 'Not_Expanded'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Timesheet', 'Not_Expanded'];
|
||||
private const array GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Not_Expanded'];
|
||||
public const GROUPS_ENTITY_FULL = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Expanded'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'Timesheet', 'Not_Expanded'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Timesheet', 'Not_Expanded'];
|
||||
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -38,10 +38,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'User')]
|
||||
final class UserController extends BaseApiController
|
||||
{
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
private const array GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
|
||||
private const array GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
|
||||
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
@@ -209,7 +209,7 @@ final class UserController extends BaseApiController
|
||||
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Success if the token could be deleted.')])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The API token ID to remove', required: true)]
|
||||
#[Route(methods: ['DELETE'], path: '/api-token/{id}', name: 'delete_api_token', requirements: ['id' => '\d+'])]
|
||||
public function deleteAccessToken(AccessToken $accessToken, AccessTokenRepository $accessTokenRepository): Response
|
||||
public function deleteApiToken(AccessToken $accessToken, AccessTokenRepository $accessTokenRepository): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
if (!$this->isGranted('api-token', $user)) {
|
||||
|
||||
@@ -63,13 +63,16 @@ class ActivityService
|
||||
public function saveActivity(Activity $activity): Activity
|
||||
{
|
||||
if ($activity->isNew()) {
|
||||
return $this->saveNewActivity($activity);
|
||||
return $this->saveNewActivity($activity); // @phpstan-ignore method.deprecated
|
||||
} else {
|
||||
return $this->updateActivity($activity);
|
||||
return $this->updateActivity($activity); // @phpstan-ignore method.deprecated
|
||||
}
|
||||
}
|
||||
|
||||
private function saveNewActivity(Activity $activity): Activity
|
||||
/**
|
||||
* @deprecated since 2.35 - use saveActivity() instead
|
||||
*/
|
||||
public function saveNewActivity(Activity $activity): Activity
|
||||
{
|
||||
if (null !== $activity->getId()) {
|
||||
throw new InvalidArgumentException('Cannot create activity, already persisted');
|
||||
@@ -103,7 +106,10 @@ class ActivityService
|
||||
}
|
||||
}
|
||||
|
||||
private function updateActivity(Activity $activity): Activity
|
||||
/**
|
||||
* @deprecated since 2.35 - use saveActivity() instead
|
||||
*/
|
||||
public function updateActivity(Activity $activity): Activity
|
||||
{
|
||||
$this->validateActivity($activity);
|
||||
|
||||
|
||||
@@ -9,22 +9,13 @@
|
||||
|
||||
namespace App\Audit;
|
||||
|
||||
/**
|
||||
* For entity classes, whose changes could be logged.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
final class Loggable
|
||||
{
|
||||
/**
|
||||
* @param class-string|null $customFieldClass
|
||||
* @param string[] $ignoredProperties
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $customFieldClass = null,
|
||||
public array $ignoredProperties = [],
|
||||
public ?string $title = null,
|
||||
public string $translationDomain = 'messages',
|
||||
)
|
||||
public function __construct(public ?string $customFieldClass = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
|
||||
namespace App\Audit;
|
||||
|
||||
/**
|
||||
* Marks a property as sensitive, so it will not be logged.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
final class SensitiveProperty
|
||||
final class Versioned
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user