Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2ce42523a | ||
|
|
b2dc5b1035 | ||
|
|
752d0cfbf3 | ||
|
|
32c8ec2aaf | ||
|
|
2ffdaa088f | ||
|
|
2fafaabe40 | ||
|
|
b1d86b4836 | ||
|
|
384fe8b8ab | ||
|
|
d429c56687 | ||
|
|
8c52eac9f0 | ||
|
|
59ecbbb591 | ||
|
|
2e54a56996 | ||
|
|
6b2a56f492 | ||
|
|
5d48d463d9 | ||
|
|
f295d6e30e | ||
|
|
162acf37a8 | ||
|
|
37c741c48d | ||
|
|
bbdcf16a5d | ||
|
|
dfb0134137 | ||
|
|
6a86afb5fd | ||
|
|
677f8d776c | ||
|
|
9e87fc131b | ||
|
|
5a0c783a76 | ||
|
|
26361da873 | ||
|
|
26f8abfcc6 | ||
|
|
27c2b912f7 | ||
|
|
7e72f144cb | ||
|
|
3c3d6379a8 |
@@ -1,23 +1,17 @@
|
||||
; top-most EditorConfig file
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
; Unix-style newlines
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
|
||||
[*.twig]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.yaml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[{compose.yaml,compose.*.yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
63
.env.dist
63
.env.dist
@@ -1,7 +1,18 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# 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!
|
||||
# * .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
|
||||
#
|
||||
# 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.
|
||||
@@ -16,32 +27,34 @@
|
||||
# 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.5.8-MariaDB":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
# 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
|
||||
#
|
||||
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
|
||||
|
||||
#================================================================================
|
||||
# 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
|
||||
|
||||
#================================================================================
|
||||
# unlikely, that you need to change this one
|
||||
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
||||
###> 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 ###
|
||||
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -5,8 +5,10 @@ tests export-ignore
|
||||
.codecov.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
eslint.config.js export-ignore
|
||||
eslint.config.mjs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.php-cs-fixer.dist.php export-ignore
|
||||
php-cs-fixer.dist.php export-ignore
|
||||
babel.config.js export-ignore
|
||||
package.json export-ignore
|
||||
@@ -15,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
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -53,6 +53,7 @@ body:
|
||||
attributes:
|
||||
label: Which PHP version are you using?
|
||||
options:
|
||||
- "8.5"
|
||||
- "8.4"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
|
||||
2
.github/release-drafter.yml
vendored
2
.github/release-drafter.yml
vendored
@@ -22,7 +22,7 @@ version-resolver:
|
||||
- 'translation'
|
||||
default: patch
|
||||
template: |
|
||||
**Compatible with PHP 8.1 to 8.4**
|
||||
**Compatible with PHP 8.1 to 8.5**
|
||||
|
||||
$CHANGES
|
||||
|
||||
|
||||
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
|
||||
@@ -60,7 +60,6 @@ jobs:
|
||||
tags: |
|
||||
kimai/kimai2:latest
|
||||
kimai/kimai2:fpm
|
||||
kimai/kimai2:fpm-${{ env.kimai_version }}
|
||||
push: true
|
||||
|
||||
- name: Apache image
|
||||
|
||||
8
.github/workflows/testing.yaml
vendored
8
.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.1', '8.2', '8.3', '8.4']
|
||||
php: ['8.4', '8.5']
|
||||
|
||||
name: Integration (${{ matrix.php }})
|
||||
steps:
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Full test-suite
|
||||
if: matrix.php != '8.2'
|
||||
if: matrix.php != '8.5'
|
||||
run: vendor/bin/phpunit tests/
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Full test-suite with coverage
|
||||
if: matrix.php == '8.2'
|
||||
if: matrix.php == '8.5'
|
||||
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Upload code coverage
|
||||
if: matrix.php == '8.2'
|
||||
if: matrix.php == '8.5'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,8 +1,9 @@
|
||||
# some hosters require a htaccess to change the PHP version
|
||||
/public/.htaccess
|
||||
/.env-*
|
||||
/.idea/
|
||||
.DS_Store
|
||||
var/templates/
|
||||
|
||||
|
||||
# custom apache rules e.g. to deactivate ioncube loader
|
||||
/public/.user.ini
|
||||
@@ -14,6 +15,7 @@
|
||||
# YARN 2
|
||||
/.yarnrc.yml
|
||||
/.yarn
|
||||
/.pnp.*
|
||||
|
||||
# for keeping empty directories
|
||||
/config/packages/local.yaml
|
||||
@@ -21,6 +23,7 @@
|
||||
/var/dev/*
|
||||
/var/data/*
|
||||
/var/cache/*
|
||||
/var/share/*
|
||||
/var/invoices*
|
||||
/var/export*
|
||||
/var/log/*
|
||||
@@ -34,7 +37,6 @@
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
.env
|
||||
/public/bundles/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
@@ -45,9 +47,8 @@
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
.php-cs-fixer.cache
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
|
||||
@@ -10,7 +10,7 @@ Send your ideas, code reviews, pull requests and feature requests to help to imp
|
||||
- Make your changes in a new git branch, based on the latest code in `main`
|
||||
- Apply our code-style by running `composer codestyle-fix`
|
||||
- Run the static code analysis with `composer phpstan`
|
||||
- Verify everything still works with `composer tests-unit`
|
||||
- Verify everything still works with `composer tests`
|
||||
- Add tests for your changes
|
||||
|
||||
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
|
||||
|
||||
@@ -29,7 +29,7 @@ and so much more.
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.1.3 minimum (support for PHP 8.2, 8.3, 8.4)
|
||||
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5
|
||||
- MariaDB or MySQL
|
||||
- A webserver and subdomain (subdirectory is not supported)
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
||||
@@ -44,7 +44,7 @@ and so much more.
|
||||
|
||||
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
|
||||
|
||||
And if you don't want to host Kimai, you can use [the Cloud version](https://www.kimai.cloud/) of it.
|
||||
And if you don't want to host Kimai, you can use the [Cloud version](https://www.kimai.cloud/) of it.
|
||||
|
||||
### Updating Kimai
|
||||
|
||||
|
||||
50
UPGRADING-2.md
Normal file
50
UPGRADING-2.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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,14 +11,27 @@ 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
|
||||
|
||||
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`
|
||||
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`
|
||||
|
||||
51
UPGRADING.md
51
UPGRADING.md
@@ -1,50 +1,3 @@
|
||||
# Upgrading Kimai - Version 2.x
|
||||
# Upgrading Kimai
|
||||
|
||||
_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`
|
||||
The most recent version is Kimai 3, please read the changelog at [UPGRADING-3.md](UPGRADING-3.md).
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
|
||||
/**
|
||||
* @deprecated use invoice-pdf instead
|
||||
*/
|
||||
require('./sass/_invoice.scss');
|
||||
|
||||
@@ -43,6 +43,7 @@ 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 {
|
||||
|
||||
@@ -76,6 +77,7 @@ 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());
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class KimaiAutocompleteTags extends KimaiAutocomplete {
|
||||
API.get(apiUrl, {'name': query}, (data) => {
|
||||
let results = [];
|
||||
for (let item of data) {
|
||||
results.push({text: item.name, value: item.name, color: item.color});
|
||||
results.push({text: item.name, value: item.name, color: item['color-safe']});
|
||||
}
|
||||
callback(results);
|
||||
}, () => {
|
||||
|
||||
44
assets/js/forms/KimaiMonthPicker.js
Normal file
44
assets/js/forms/KimaiMonthPicker.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
const ALERT = this.getContainer().getPlugin('alert');
|
||||
const successHandle = () => {
|
||||
EVENTS.trigger(eventName);
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
if (attributes['msgSuccess'] !== undefined) {
|
||||
ALERT.success(attributes['msgSuccess']);
|
||||
}
|
||||
@@ -82,6 +83,7 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
if (attributes['msgError'] !== undefined) {
|
||||
message = attributes['msgError'];
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
API.handleError(message, error);
|
||||
};
|
||||
|
||||
@@ -90,6 +92,8 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
data = attributes['payload'];
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
|
||||
if (method === 'PATCH') {
|
||||
API.patch(url, data, successHandle, errorHandle);
|
||||
} else if (method === 'POST') {
|
||||
|
||||
@@ -217,8 +217,6 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
const eventName = form.dataset['formEvent'];
|
||||
/** @type {KimaiEvent} alert */
|
||||
const events = this.getContainer().getPlugin('event');
|
||||
/** @type {KimaiAlert} alert */
|
||||
const alert = this.getContainer().getPlugin('alert');
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -259,15 +257,8 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
} else {
|
||||
events.trigger(eventName);
|
||||
|
||||
// try to find form defined message first, but
|
||||
let msg = form.dataset['msgSuccess'];
|
||||
// if that is not available: use a generic fallback message
|
||||
if (msg === null || msg === undefined || msg === '') {
|
||||
msg = 'action.update.success';
|
||||
}
|
||||
this._isDirty = false;
|
||||
this._getModal().hide();
|
||||
alert.success(msg);
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -277,6 +268,8 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
message = 'action.update.error';
|
||||
}
|
||||
|
||||
/** @type {KimaiAlert} alert */
|
||||
const alert = this.getContainer().getPlugin('alert');
|
||||
alert.error(message, error.message);
|
||||
|
||||
// this is useful for changing form fields and retrying to save (and in development to test form changes)
|
||||
|
||||
@@ -42,7 +42,7 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
|
||||
}
|
||||
|
||||
// at which element we append the loading screen
|
||||
let container = 'body';
|
||||
let container = 'div.page-wrapper';
|
||||
if (event.detail !== undefined && event.detail !== null) {
|
||||
container = event.detail;
|
||||
}
|
||||
|
||||
@@ -70,8 +70,6 @@ export default class KimaiCalendar {
|
||||
const DATES = this.kimai.getPlugin('date');
|
||||
/** @type {KimaiAjaxModalForm} MODAL */
|
||||
const MODAL = this.kimai.getPlugin('modal');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = this.kimai.getPlugin('alert');
|
||||
|
||||
// Instead of using "buttonIcons" the theme needs to be adjusted directly
|
||||
// https://fullcalendar.io/docs/buttonIcons
|
||||
@@ -282,6 +280,7 @@ export default class KimaiCalendar {
|
||||
droppable: true,
|
||||
// drop function handles external draggable events
|
||||
drop: (dropInfo) => {
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
const entry = dropInfo.draggedEl;
|
||||
const source = entry.parentElement;
|
||||
let data = JSON.parse(entry.dataset.entry);
|
||||
@@ -324,7 +323,7 @@ export default class KimaiCalendar {
|
||||
(result) => {
|
||||
const newItem = this.convertSourceForCalendar(result);
|
||||
this.getCalendar().addEvent(newItem, true);
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -334,7 +333,7 @@ export default class KimaiCalendar {
|
||||
(result) => {
|
||||
const newItem = this.convertSourceForCalendar(result);
|
||||
this.getCalendar().addEvent(newItem, true);
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -703,8 +702,6 @@ export default class KimaiCalendar {
|
||||
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = this.kimai.getPlugin('api');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = this.kimai.getPlugin('alert');
|
||||
/** @type {KimaiDateUtils} DATE */
|
||||
const DATES = this.kimai.getPlugin('date');
|
||||
|
||||
@@ -716,11 +713,14 @@ export default class KimaiCalendar {
|
||||
payload.end = null;
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
|
||||
const updateUrl = this.options.url.update(event.id);
|
||||
API.patch(updateUrl, JSON.stringify(payload), () => {
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}, (error) => {
|
||||
eventArg.revert();
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
API.handleError('action.update.error', error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1021;
|
||||
background-color: var(--tblr-backdrop-bg);
|
||||
/*background-color: var(--tblr-backdrop-bg);*/
|
||||
opacity: $modal-backdrop-opacity;
|
||||
}
|
||||
|
||||
@@ -48,4 +48,12 @@ fieldset.form-fieldset > legend {
|
||||
.page-title {
|
||||
color: var(--tblr-body-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlighted text is not visible - https://github.com/tabler/tabler/issues/2603 */
|
||||
[data-bs-theme=dark] {
|
||||
::selection,
|
||||
.text-selected {
|
||||
background-color: var(--#{$prefix}primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use App\ConsoleApplication;
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "8.1.*||8.2.*||8.3.*||8.4.*",
|
||||
"php": "8.4.*||8.5.*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
@@ -29,11 +29,10 @@
|
||||
"composer/semver": "^3.3",
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^2.8",
|
||||
"doctrine/orm": "^3.0",
|
||||
"easybill/zugferd-php": "^2.1",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"endroid/qr-code": "^6.0",
|
||||
"friendsofsymfony/rest-bundle": "3.9.0-beta1",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"horstoeko/zugferd": "^1.0",
|
||||
"horstoeko/zugferdublbridge": "^1.0",
|
||||
@@ -45,37 +44,39 @@
|
||||
"nelmio/cors-bundle": "^2.0",
|
||||
"onelogin/php-saml": "^4.0",
|
||||
"openspout/openspout": "^4.0",
|
||||
"pagerfanta/pagerfanta": "^3.0",
|
||||
"pagerfanta/pagerfanta": "^4.0",
|
||||
"parsedown/parsedown": "^1.6",
|
||||
"phpoffice/phpspreadsheet": "^2.0",
|
||||
"phpoffice/phpword": "^1.0",
|
||||
"psr/container": "^2.0",
|
||||
"psr/log": "^3.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",
|
||||
"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",
|
||||
"symfony/flex": "^2",
|
||||
"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/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/monolog-bundle": "^3.4",
|
||||
"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/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/webpack-encore-bundle": "^2.0",
|
||||
"symfony/yaml": "^6.0",
|
||||
"symfony/yaml": "^7.0",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
"twig/inky-extra": "^3.0",
|
||||
@@ -95,14 +96,14 @@
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/phpunit": "^10.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"
|
||||
"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"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
@@ -117,7 +118,7 @@
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"platform": {
|
||||
"php": "8.1.3"
|
||||
"php": "8.4.4"
|
||||
},
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
@@ -144,6 +145,9 @@
|
||||
"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": "*",
|
||||
@@ -209,7 +213,7 @@
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "6.4.*"
|
||||
"require": "7.4.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3442
composer.lock
generated
3442
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, 'test' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => 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
@@ -1,6 +0,0 @@
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
packages:
|
||||
avatars:
|
||||
base_path: 'avatars'
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
5
config/packages/dama_doctrine_test_bundle.yaml
Normal file
5
config/packages/dama_doctrine_test_bundle.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
when@test:
|
||||
dama_doctrine_test:
|
||||
enable_static_connection: true
|
||||
enable_static_meta_data_cache: true
|
||||
enable_static_query_cache: true
|
||||
5
config/packages/debug.yaml
Normal file
5
config/packages/debug.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
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,29 +10,31 @@ 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(DATABASE_URL)%'
|
||||
url: '%env(resolve: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:
|
||||
# FIXME this is causing a deprecation and needs to be changed
|
||||
# auto_mapping: false
|
||||
auto_mapping: true
|
||||
auto_generate_proxy_classes: '%kernel.debug%'
|
||||
auto_mapping: false
|
||||
auto_generate_proxy_classes: true
|
||||
default_entity_manager: default
|
||||
enable_lazy_ghost_objects: true
|
||||
enable_native_lazy_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
|
||||
@@ -56,17 +58,23 @@ when@test:
|
||||
connections:
|
||||
default:
|
||||
logging: false
|
||||
use_savepoints: true
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
metadata_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
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,8 +1,11 @@
|
||||
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'
|
||||
migrations_paths:
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'
|
||||
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Read the documentation: https://fosrestbundle.readthedocs.io/en/3.x/
|
||||
fos_rest:
|
||||
param_fetcher_listener:
|
||||
enabled: true
|
||||
|
||||
@@ -37,6 +37,9 @@ framework:
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
|
||||
mailer:
|
||||
dsn: '%env(MAILER_URL)%'
|
||||
|
||||
|
||||
@@ -3,10 +3,6 @@ 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:
|
||||
@@ -18,3 +14,20 @@ 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,5 +1,50 @@
|
||||
monolog:
|
||||
channels: ["deprecation"]
|
||||
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
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
@@ -24,42 +69,3 @@ 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', 'X-AUTH-USER', 'X-AUTH-TOKEN']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
# expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
parameters:
|
||||
env(default_uri): 'http://localhost'
|
||||
|
||||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
@@ -5,7 +8,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: http://localhost
|
||||
default_uri: '%env(DEFAULT_URI)%'
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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:
|
||||
@@ -11,12 +12,11 @@ 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,66 +27,52 @@ 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}
|
||||
@@ -99,13 +85,12 @@ 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
|
||||
# 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
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
twig:
|
||||
debug: '%kernel.debug%'
|
||||
strict_variables: '%kernel.debug%'
|
||||
file_name_pattern: '*.twig'
|
||||
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,15 +1,13 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
profiler:
|
||||
collect: false
|
||||
collect_serializer_data: true
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
webpack_encore:
|
||||
# The path where Encore is building the assets.
|
||||
# This should match Encore.setOutputPath() in webpack.config.js.
|
||||
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
output_path: '%kernel.project_dir%/public/build'
|
||||
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||
# output_path: false
|
||||
|
||||
# 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%'
|
||||
# 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
|
||||
|
||||
when@dev:
|
||||
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# crossorigin: 'anonymous'
|
||||
|
||||
# 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:
|
||||
webpack_encore:
|
||||
cache: false
|
||||
# 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
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
# 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}
|
||||
@@ -8,7 +18,8 @@ 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/
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /{_locale}/_error
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
@@ -1,7 +0,0 @@
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
6
config/routes/framework.yaml
Normal file
6
config/routes/framework.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||
prefix: /{_locale}/_error
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
3
config/routes/security.yaml
Normal file
3
config/routes/security.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
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,3 +1,11 @@
|
||||
# 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
|
||||
@@ -6,8 +14,8 @@ parameters:
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
public: false
|
||||
bind:
|
||||
$projectDirectory: '%kernel.project_dir%'
|
||||
@@ -16,12 +24,11 @@ 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/'
|
||||
@@ -35,6 +42,9 @@ 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\:
|
||||
@@ -227,3 +237,13 @@ 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}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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
Normal file
0
migrations/.gitignore
vendored
Normal file
@@ -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');
|
||||
$timesheetTags->setPrimaryKey(['timesheet_id', 'tag_id']);
|
||||
$this->addPrimaryKeyConstraint($timesheetTags, ['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');
|
||||
$tags->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($tags, ['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]);
|
||||
$timesheetMeta->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($timesheetMeta, ['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]);
|
||||
$customerMeta->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($customerMeta, ['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]);
|
||||
$projectMeta->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($projectMeta, ['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]);
|
||||
$activityMeta->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($activityMeta, ['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]);
|
||||
$teams->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($teams, ['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');
|
||||
$userTeams->setPrimaryKey(['user_id', 'team_id']);
|
||||
$this->addPrimaryKeyConstraint($userTeams, ['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');
|
||||
$customerTeams->setPrimaryKey(['customer_id', 'team_id']);
|
||||
$this->addPrimaryKeyConstraint($customerTeams, ['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');
|
||||
$projectTeams->setPrimaryKey(['project_id', 'team_id']);
|
||||
$this->addPrimaryKeyConstraint($projectTeams, ['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]);
|
||||
$roles->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($roles, ['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]);
|
||||
$rolePermissions->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($rolePermissions, ['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]);
|
||||
$customerComment->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($customerComment, ['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]);
|
||||
$projectComment->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($projectComment, ['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');
|
||||
$customerRates->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($customerRates, ['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');
|
||||
$projectRates->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($projectRates, ['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');
|
||||
$activityRates->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($activityRates, ['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');
|
||||
$invoices->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($invoices, ['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]);
|
||||
$sessions->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($sessions, ['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');
|
||||
$activityTeams->setPrimaryKey(['activity_id', 'team_id']);
|
||||
$this->addPrimaryKeyConstraint($activityTeams, ['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');
|
||||
$bookmarks->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($bookmarks, ['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();
|
||||
$teamMember->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($teamMember, ['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');
|
||||
$teamMember->setPrimaryKey(['user_id', 'team_id']);
|
||||
$this->addPrimaryKeyConstraint($teamMember, ['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]);
|
||||
$table->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($table, ['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]);
|
||||
$workingTimes->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($workingTimes, ['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]);
|
||||
|
||||
$accessTokens->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($accessTokens, ['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]);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$this->addPrimaryKeyConstraint($table, ['id']);
|
||||
$table->addUniqueIndex(['title'], 'UNIQ_2F0CA26F2B36786B');
|
||||
}
|
||||
|
||||
|
||||
109
migrations/Version20260101010000.php
Normal file
109
migrations/Version20260101010000.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?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,58 +1,59 @@
|
||||
{
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
268
phpstan.neon
268
phpstan.neon
@@ -49,16 +49,6 @@ 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
|
||||
@@ -179,11 +169,6 @@ 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
|
||||
@@ -404,11 +389,6 @@ 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
|
||||
@@ -594,21 +574,6 @@ 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
|
||||
@@ -1174,21 +1139,11 @@ 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
|
||||
@@ -1264,21 +1219,6 @@ 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
|
||||
@@ -1364,11 +1304,6 @@ 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
|
||||
@@ -1429,16 +1364,6 @@ 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
|
||||
@@ -1939,61 +1864,6 @@ 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
|
||||
@@ -2644,16 +2514,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/Type/UserPreferenceType.php
|
||||
|
||||
-
|
||||
message: "#^PHPDoc tag @var for variable \\$collection contains generic class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection but does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Form/Type/UserPreferencesCollectionType.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$key of method Doctrine\\\\Common\\\\Collections\\\\ArrayCollection\\<\\(int\\|string\\),mixed\\>\\:\\:set\\(\\) expects \\(int\\|string\\), string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Type/UserPreferencesCollectionType.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset mixed on mixed\\.$#"
|
||||
count: 1
|
||||
@@ -3331,7 +3191,7 @@ parameters:
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$identifier of method App\\\\Entity\\\\User\\:\\:setUserIdentifier\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 2
|
||||
count: 1
|
||||
path: src/Saml/SamlProvider.php
|
||||
|
||||
-
|
||||
@@ -3339,16 +3199,6 @@ 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
|
||||
@@ -3485,34 +3335,24 @@ parameters:
|
||||
path: src/Twig/Context.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:checkInColumDefinition\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:checkInColumDefinition\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:getClass\\(\\) has parameter \\$class with no type specified\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:getDatatableColumnClass\\(\\) should return string but returns bool\\|string\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:getDatatableColumnClass\\(\\) should return string but returns bool\\|string\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) has parameter \\$defaultColumns with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Twig/DatatableExtensions.php
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) has parameter \\$defaultColumns with no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method App\\\\Twig\\\\Runtime\\\\DatatableExtensions\\:\\:initializeDatatable\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
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
|
||||
path: src/Twig/Runtime/DatatableExtensions.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Extensions\\:\\:replaceNewline\\(\\) has no return type specified\\.$#"
|
||||
@@ -3549,21 +3389,6 @@ 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
|
||||
@@ -3604,11 +3429,6 @@ 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
|
||||
@@ -3619,11 +3439,6 @@ 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
|
||||
@@ -3764,11 +3579,6 @@ 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
|
||||
@@ -3819,61 +3629,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Utils/ProfileManager.php
|
||||
|
||||
-
|
||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 0 on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$body on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$draft on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$html_url on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$prerelease on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$published_at on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$tag_name on mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$zipball_url on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Utils\\\\ReleaseVersion\\:\\:getReleasesFromGithub\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'pattern' on mixed\\.$#"
|
||||
count: 1
|
||||
@@ -4008,8 +3763,3 @@ parameters:
|
||||
message: "#^Method App\\\\Form\\\\MultiUpdate\\\\MultiUpdateTableDTO\\:\\:setEntities\\(\\) has parameter \\$entities with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection but does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Form/MultiUpdate/MultiUpdateTableDTO.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\InvoiceTemplate\\:\\:getMetaFields\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Entity/InvoiceTemplate.php
|
||||
|
||||
139
phpunit.xml.dist
139
phpunit.xml.dist
@@ -1,8 +1,12 @@
|
||||
<?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="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
cacheDirectory="var/cache/phpunit/"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
@@ -10,59 +14,86 @@
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
displayDetailsOnPhpunitDeprecations="true">
|
||||
<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_US"/>
|
||||
<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!
|
||||
<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!
|
||||
|
||||
- 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"/>
|
||||
</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>
|
||||
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>
|
||||
</phpunit>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<tile>
|
||||
<square150x150logo src="favicon/mstile-150x150.png"/>
|
||||
<square310x310logo src="favicon/mstile-large.jpg"/>
|
||||
<TileColor>#00a300</TileColor>
|
||||
<TileColor>#ffffff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
File diff suppressed because one or more lines are too long
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.3ba0bd00.js
Normal file
2
public/build/app.3ba0bd00.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
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app.9662939e.js"
|
||||
"/build/app.3ba0bd00.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.c18ba3c6.css"
|
||||
"/build/app.16ea604e.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
@@ -15,7 +15,7 @@
|
||||
"/build/app-rtl.15853b82.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.2003dce5.css"
|
||||
"/build/app-rtl.f149299b.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -54,7 +54,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/calendar.b2d70caa.js"
|
||||
"/build/calendar.4242f779.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.d757753e.css"
|
||||
@@ -81,10 +81,10 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
||||
"/build/app.9662939e.js": "sha384-bRL78SVI1wjTweqKeb+ZSSbA78i6By89S8/xx+55zmbOVtF1TCu3lf3pvdki5ZVJ",
|
||||
"/build/app.c18ba3c6.css": "sha384-qkIgqLzngG2NchdFVImbGMU49lWlZg6Y9D0z2P0u1j2NQstDicA4KqIkkNYcpErm",
|
||||
"/build/app.3ba0bd00.js": "sha384-4G73IK+GVsttYdvW0Pt2Hov27rTJuGDusRBvoRnzPezDlTiArF4MV9mVDi7anlX+",
|
||||
"/build/app.16ea604e.css": "sha384-9734FGgqgVw+kNVry5jl15+pbaetOtfl3eywAy2pSigGpeIXlCh1udhh7rI9lNKo",
|
||||
"/build/app-rtl.15853b82.js": "sha384-UnKKgLMu9FnRT+CFE0no/+UiUks012bYriQdUWa6f02mo6Lswl947mPybjvKL503",
|
||||
"/build/app-rtl.2003dce5.css": "sha384-pl8GyGo8sRRw1zLh9D42ZAvo450onfxeszZWocUUuzq70jAOwOlaoPYh/yyc20U7",
|
||||
"/build/app-rtl.f149299b.css": "sha384-nCTYbFHUkkF20RA5oXqGah3iDnnSUxAmuFHpbehTzHszCgJBVRoHC81Q5OwFreLf",
|
||||
"/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",
|
||||
@@ -92,7 +92,7 @@
|
||||
"/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.b2d70caa.js": "sha384-FS7Q9iCWHpo2Nzk0tmNvnuTbxJKLdbXFba9F6WcXfHbKFAS2Q+lW29H9Mv+ap5kA",
|
||||
"/build/calendar.4242f779.js": "sha384-fz1ouLGHV92+3gq3eWYAgEIbwVSmuCfILeVjAnnXGjJJfUtMs+qlGEIIo3uQa0rz",
|
||||
"/build/calendar.d757753e.css": "sha384-cTmQMgHYjd2gfObFWmEUph7qQLCyXaIkneSf+bQ2mqVmZwqOB+pJOm/UYTyTjALJ",
|
||||
"/build/dashboard.6ce7ac9c.js": "sha384-5BOoyjZx/ZKr9IbogWfT/aBRHx7XA1fePADdUpUNlSF/rqVo3taXLLcrIoaDZ+8b",
|
||||
"/build/dashboard.b7129fa1.css": "sha384-2nn5hLA+3YedgHYBpge62S8Losj8aoPwK9Zk9EvN1xEYatvOUQ7H3rIR2UUJAGOS",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build/app.css": "/build/app.c18ba3c6.css",
|
||||
"build/app.js": "/build/app.9662939e.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.2003dce5.css",
|
||||
"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-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",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.26d98626.js",
|
||||
"build/chart.js": "/build/chart.bafa38e7.js",
|
||||
"build/calendar.css": "/build/calendar.d757753e.css",
|
||||
"build/calendar.js": "/build/calendar.b2d70caa.js",
|
||||
"build/calendar.js": "/build/calendar.4242f779.js",
|
||||
"build/dashboard.css": "/build/dashboard.b7129fa1.css",
|
||||
"build/dashboard.js": "/build/dashboard.6ce7ac9c.js",
|
||||
"build/highlight.css": "/build/highlight.98bf3927.css",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
],
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"theme-color": "#1d273b",
|
||||
"theme-color": "#262626",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -35,9 +35,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'Activity')]
|
||||
final class ActivityController extends BaseApiController
|
||||
{
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Activity', 'Activity_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Activity'];
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
|
||||
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 function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -26,22 +26,12 @@ 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
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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'];
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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;
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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
|
||||
{
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Customer', 'Customer_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Customer'];
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
|
||||
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 function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -28,8 +28,8 @@ use Symfony\Component\Validator\Constraints;
|
||||
#[OA\Tag(name: 'Invoice')]
|
||||
final class InvoiceController extends BaseApiController
|
||||
{
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
|
||||
private const array GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
|
||||
private const array GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
@@ -43,8 +43,8 @@ final class InvoiceController extends BaseApiController
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))]
|
||||
#[Route(methods: ['GET'], path: '', name: 'get_invoices')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only invoices created at or after this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only invoices created before or at this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')]
|
||||
#[Rest\QueryParam(name: 'status', map: true, requirements: 'pending|paid|canceled|new', strict: true, nullable: true, default: [], description: 'Invoice status: pending, paid, canceled, new. Default: all')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
|
||||
@@ -37,9 +37,9 @@ use Symfony\Component\Validator\Constraints;
|
||||
#[OA\Tag(name: 'Project')]
|
||||
final class ProjectController extends BaseApiController
|
||||
{
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Project', 'Project_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Project'];
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
|
||||
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 function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -24,44 +24,40 @@ 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(methods: ['GET'], path: '/ping')]
|
||||
public function pingAction(): Response
|
||||
#[Route(path: '/ping', methods: ['GET'])]
|
||||
public function pingAction(ViewHandlerInterface $viewHandler): Response
|
||||
{
|
||||
$view = new View(['message' => 'pong'], 200);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
return $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(methods: ['GET'], path: '/version')]
|
||||
public function versionAction(): Response
|
||||
#[Route(path: '/version', methods: ['GET'])]
|
||||
public function versionAction(ViewHandlerInterface $viewHandler): Response
|
||||
{
|
||||
return $this->viewHandler->handle(new View(new Version(), 200));
|
||||
return $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(methods: ['GET'], path: '/plugins')]
|
||||
public function pluginAction(PluginManager $pluginManager): Response
|
||||
#[Route(path: '/plugins', methods: ['GET'])]
|
||||
public function pluginAction(PluginManager $pluginManager, ViewHandlerInterface $viewHandler): Response
|
||||
{
|
||||
$plugins = [];
|
||||
foreach ($pluginManager->getPlugins() as $plugin) {
|
||||
$plugins[] = new Plugin($plugin);
|
||||
}
|
||||
|
||||
return $this->viewHandler->handle(new View($plugins, 200));
|
||||
return $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
|
||||
{
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Tag'];
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Tag'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'Tag'];
|
||||
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 function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
@@ -39,9 +39,9 @@ final class TagController extends BaseApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated: Fetch tags as strings
|
||||
* Fetch tags as strings
|
||||
*/
|
||||
#[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\Response(response: 200, description: '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
|
||||
{
|
||||
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'];
|
||||
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 function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
|
||||
@@ -47,11 +47,11 @@ use Symfony\Component\Validator\Constraints;
|
||||
#[OA\Tag(name: 'Timesheet')]
|
||||
final class TimesheetController extends BaseApiController
|
||||
{
|
||||
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'];
|
||||
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 function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
@@ -86,8 +86,8 @@ final class TimesheetController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'tags', map: true, strict: true, nullable: true, default: [], description: 'List of tag names, e.g. tags[]=bar&tags[]=foo')]
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|begin|end|rate', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)')]
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: DESC)')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records started at or after this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records started at or before this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
||||
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
||||
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
||||
@@ -380,7 +380,7 @@ final class TimesheetController extends BaseApiController
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
|
||||
#[Route(methods: ['GET'], path: '/recent', name: 'recent_timesheet')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records started at or after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries (default: 10)')]
|
||||
public function recentAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
|
||||
@@ -38,10 +38,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'User')]
|
||||
final class UserController extends BaseApiController
|
||||
{
|
||||
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'];
|
||||
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 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 deleteApiToken(AccessToken $accessToken, AccessTokenRepository $accessTokenRepository): Response
|
||||
public function deleteAccessToken(AccessToken $accessToken, AccessTokenRepository $accessTokenRepository): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
if (!$this->isGranted('api-token', $user)) {
|
||||
|
||||
@@ -63,16 +63,13 @@ class ActivityService
|
||||
public function saveActivity(Activity $activity): Activity
|
||||
{
|
||||
if ($activity->isNew()) {
|
||||
return $this->saveNewActivity($activity); // @phpstan-ignore method.deprecated
|
||||
return $this->saveNewActivity($activity);
|
||||
} else {
|
||||
return $this->updateActivity($activity); // @phpstan-ignore method.deprecated
|
||||
return $this->updateActivity($activity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.35 - use saveActivity() instead
|
||||
*/
|
||||
public function saveNewActivity(Activity $activity): Activity
|
||||
private function saveNewActivity(Activity $activity): Activity
|
||||
{
|
||||
if (null !== $activity->getId()) {
|
||||
throw new InvalidArgumentException('Cannot create activity, already persisted');
|
||||
@@ -106,10 +103,7 @@ class ActivityService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.35 - use saveActivity() instead
|
||||
*/
|
||||
public function updateActivity(Activity $activity): Activity
|
||||
private function updateActivity(Activity $activity): Activity
|
||||
{
|
||||
$this->validateActivity($activity);
|
||||
|
||||
|
||||
@@ -9,13 +9,22 @@
|
||||
|
||||
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 function __construct(
|
||||
public ?string $customFieldClass = null,
|
||||
public array $ignoredProperties = [],
|
||||
public ?string $title = null,
|
||||
public string $translationDomain = 'messages',
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
|
||||
namespace App\Audit;
|
||||
|
||||
/**
|
||||
* Marks a property as sensitive, so it will not be logged.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
final class Versioned
|
||||
final class SensitiveProperty
|
||||
{
|
||||
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