Compare commits
18 Commits
2.0.0-alph
...
2.0.0-rc-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f32448b9f5 | ||
|
|
f3b4078c6a | ||
|
|
ca846b5fbf | ||
|
|
63a3ae1147 | ||
|
|
5711c15eac | ||
|
|
a705703996 | ||
|
|
326dc64f65 | ||
|
|
974e1b3b5a | ||
|
|
f7f1765c2e | ||
|
|
aae31c567c | ||
|
|
49f307d605 | ||
|
|
f93d081a88 | ||
|
|
a230be77dd | ||
|
|
b62253e1f3 | ||
|
|
0e91dd886e | ||
|
|
6e0500972e | ||
|
|
8069e332fe | ||
|
|
cbd65f1f1d |
@@ -9,7 +9,7 @@ coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 2.5%
|
||||
threshold: 0.5%
|
||||
patch: off
|
||||
changes: no
|
||||
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -64,7 +64,6 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
render: Text
|
||||
attributes:
|
||||
label: Logfile
|
||||
description: Please paste the last lines from your logfile at "var/log/prod.log" or "Doctor > Logs", around the time when the problem happened.
|
||||
|
||||
6
.github/release-drafter.yml
vendored
@@ -35,11 +35,7 @@ version-resolver:
|
||||
template: |
|
||||
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://tobybatch.github.io/kimai2/)
|
||||
|
||||
**PHP Version compatibility:**
|
||||
- PHP 7.3 and PHP 7.4 are [end-of-life](https://www.php.net/supported-versions.php)
|
||||
- PHP 8.0 and PHP 8.1 are supported
|
||||
|
||||
A feature freeze is in place and only bugfix releases will be published for 1.30.x. Next major release will be in the 2.x series (PHP >= 8.1, Symfony 6, Tabler UI, see #2902).
|
||||
**Compatible with PHP 8.1 and 8.2**
|
||||
|
||||
$CHANGES
|
||||
|
||||
|
||||
4
.github/workflows/lock.yaml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
- uses: dessant/lock-threads@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '180'
|
||||
issue-inactive-days: '90'
|
||||
exclude-issue-created-before: ''
|
||||
exclude-issue-created-after: ''
|
||||
exclude-issue-created-between: ''
|
||||
|
||||
44
README.md
@@ -5,9 +5,8 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/workflows/CI/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
|
||||
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
|
||||
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
|
||||
<a href="https://twitter.com/kimai_org" rel="me"><img alt="Twitter" src="https://img.shields.io/badge/follow-%40kimai__org-00acee"></a>
|
||||
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
|
||||
<a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a>
|
||||
</p>
|
||||
|
||||
@@ -15,32 +14,31 @@
|
||||
|
||||
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
||||
It is built with modern technologies such as [Symfony](https://github.com/symfony/symfony), [Bootstrap](https://github.com/twbs/bootstrap),
|
||||
[RESTful API](https://github.com/FriendsOfSymfony/FOSRestBundle), [Doctrine](https://github.com/doctrine/),
|
||||
[Tabler](https://github.com/kevinpapst/TablerBundle/), [Webpack](https://github.com/webpack/webpack), ES6 and [many](composer.json) [more](package.json).
|
||||
[JSON API](https://github.com/FriendsOfSymfony/FOSRestBundle), [Doctrine](https://github.com/doctrine/),
|
||||
[Tabler](https://github.com/kevinpapst/TablerBundle/), ES6 and [many](composer.json) [more](package.json).
|
||||
|
||||
## Introduction
|
||||
|
||||
- [Home](https://www.kimai.org) - Kimai project homepage
|
||||
- [Blog](https://www.kimai.org/blog/) - Read the latest news
|
||||
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use Kimai
|
||||
- [Translations](https://hosted.weblate.org/projects/kimai/#languages) - Kimai in your language
|
||||
- [Migration](https://www.kimai.org/documentation/migration-v1.html) - Import data from Kimai 1
|
||||
- [Translations](https://hosted.weblate.org/projects/kimai/#languages) - Kimai is translated at Weblate
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.1 minimum
|
||||
- MariaDB or MySQL
|
||||
- A webserver and subdomain
|
||||
- A webserver and subdomain (subdirectory does not work)
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `xsl`, `zip`
|
||||
|
||||
### About
|
||||
|
||||
The evolution of the most known(?) open source project time-tracker Kimai. It is stable, production ready and ships
|
||||
with many advanced features, including but not limited to:
|
||||
Kimai is a professional grade time-tracking application, build to track your project times.
|
||||
It ships with many advanced features, including but not limited to:
|
||||
|
||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
||||
authentication via SAML/LDAP/Database, customizable role and team permissions, responsive and ready for your mobile device,
|
||||
user/customer/project specific rates, advanced search & filtering, money and time budgets, reporting, support for plugins
|
||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user - multi-timezones - multi-language,
|
||||
authentication via SAML/LDAP/Database, support for 2FA with TOTP, customizable role and team permissions, responsive and ready for your mobile device,
|
||||
user/customer/project specific rates, advanced search & filtering, money and time budgets, advanced reporting, support for plugins
|
||||
and so many more.
|
||||
|
||||
## Installation
|
||||
@@ -65,8 +63,10 @@ and so many more.
|
||||
You can see a rough development roadmap in the [Milestones](https://github.com/kimai/kimai/milestones) sections.
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kimai/kimai/issues) are welcome.
|
||||
|
||||
Release versions will be created on a regular basis, every couple of weeks.
|
||||
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
|
||||
Release versions will be created on a regular basis, every couple of weeks latest.
|
||||
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
|
||||
|
||||
For the time being and until 2.0 landed everywhere, the [1.x branch](https://github.com/kimai/kimai/tree/1.x) will receive bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -75,11 +75,11 @@ The best way to start is to [open a new issue](https://github.com/kimai/kimai/is
|
||||
|
||||
In case you want to contribute, but you wouldn't know how, here are some suggestions:
|
||||
|
||||
- Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on platforms like Slant, Product Hunt, Softpedia or AlternativeTo, you can tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every bit helps!
|
||||
- Answer questions: You know the answer to another user's problem? Share your knowledge!
|
||||
- Make a feature request: Something can be done better? Something essential missing? Let us know!
|
||||
- Report bugs
|
||||
- You don't have to be programmer to help. The documentation and translation could use some love as well.
|
||||
- Sponsor the project, free software still costs money
|
||||
- Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on any software platform, you can toot or tweet about it, share it on LinkedIn, Reddit or any of your favorite social media platforms. Every bit helps!
|
||||
- Answer questions: You know the answer to another user's problem? Share your knowledge.
|
||||
- Something can be done better? An essential feature is missing? Create a feature request.
|
||||
- Report bugs makes Kimai better for everyone.
|
||||
- You don't have to be programmer, the documentation and translation could always use some attention.
|
||||
- Sponsor the project: free software costs money to create!
|
||||
|
||||
There is one simple rule in our "Code of conduct": Don't be an ass!
|
||||
There is one simple rule in our "Code of conduct": Don't be an ass!
|
||||
|
||||
36
TODO
@@ -1,36 +0,0 @@
|
||||
===========================================================================
|
||||
|
||||
INVOICES
|
||||
|
||||
- HTML Rechnungstemplates funktionieren nicht mehr richtig
|
||||
|
||||
===========================================================================
|
||||
|
||||
THEME
|
||||
|
||||
- Update to latest release und angepasstes CSS entfernen
|
||||
|
||||
- Theme Dark Mode mit Modus "Browser" sollte Standard sein:
|
||||
https://github.com/tabler/tabler/issues/892#event-5666309557
|
||||
|
||||
===========================================================================
|
||||
|
||||
MIXED
|
||||
|
||||
- Alle URLs ändern
|
||||
- /admin/activity/ zu /activity/
|
||||
- /admin/project/ zu /project/
|
||||
- /admin/..../ zu /..../
|
||||
- /team/timesheet/ zu /timesheets/
|
||||
|
||||
- Tags => immer Berechtigung prüfen und "create" option über die VIEW ans Javascript geben
|
||||
=> den FormType nur ändern zu "AutoComplete" wenn es mehr als 500 Tags sind
|
||||
=> Übersetzungen (Suche, Erstellen, Keine Ergebnisse) über data attribute ans JS durchreichen
|
||||
|
||||
===========================================================================
|
||||
|
||||
MIGRATIONS
|
||||
|
||||
- rename 2.0 migration once it will be released
|
||||
|
||||
===========================================================================
|
||||
@@ -8,7 +8,7 @@ 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](https://github.com/kevinpapst/kimai2/releases/tag/2.0)
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
**!! This release requires minimum PHP version to 8.1 !!**
|
||||
|
||||
|
||||
@@ -71,7 +71,11 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
return Modal.getOrCreateInstance(this._getModalElement())
|
||||
}
|
||||
|
||||
openUrlInModal(url)
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {function(Response)} error the callback to execute if the fetch failed
|
||||
*/
|
||||
openUrlInModal(url, error)
|
||||
{
|
||||
const headers = new Headers();
|
||||
headers.append('X-Requested-With', 'Kimai-Modal');
|
||||
@@ -91,8 +95,12 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
this._openFormInModal(html);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
window.location = url;
|
||||
.catch((reason) => {
|
||||
if (error === undefined || error === null) {
|
||||
window.location = url;
|
||||
} else {
|
||||
error(reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -376,7 +376,18 @@ export default class KimaiCalendar {
|
||||
return;
|
||||
}
|
||||
this.hidePopover(eventClickInfo.el);
|
||||
MODAL.openUrlInModal(this.options.url.edit(event.id));
|
||||
|
||||
if (!event.extendedProps.exported || this.hasPermission('edit_exported')) {
|
||||
MODAL.openUrlInModal(
|
||||
this.options.url.edit(event.id), (reason) => {
|
||||
// 403 = user is not allowed to edit the entry (e.g. lockdown mode)
|
||||
if (reason.status !== 403) {
|
||||
// keep the log, it might help with debugging
|
||||
console.log(reason);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
}};
|
||||
|
||||
@@ -506,6 +517,8 @@ export default class KimaiCalendar {
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used on manipulated timesheets!
|
||||
*
|
||||
* @param {object} apiItem
|
||||
* @return {{activity, color: *, start, description, project, end, id, title: *, textColor: *, customer, tags: ([number,number,[],string,string]|*)}}
|
||||
* @private
|
||||
@@ -538,6 +551,7 @@ export default class KimaiCalendar {
|
||||
timesheet: apiItem.id,
|
||||
title: title,
|
||||
description: apiItem.description,
|
||||
exported: apiItem.exported,
|
||||
start: apiItem.begin,
|
||||
end: apiItem.end,
|
||||
activity: apiItem.activity.name,
|
||||
@@ -591,6 +605,12 @@ export default class KimaiCalendar {
|
||||
changeHandler(eventArg) {
|
||||
/** @type {EventApi} event */
|
||||
const event = eventArg.event;
|
||||
|
||||
if (event.extendedProps.exported && !this.hasPermission('edit_exported')) {
|
||||
eventArg.revert();
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = this.kimai.getPlugin('api');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
@import "~bootstrap/scss/variables";
|
||||
@import "~bootstrap/scss/maps";
|
||||
@import "~bootstrap/scss/mixins";
|
||||
//@import "~bootstrap/scss/normalize";
|
||||
//@import "~bootstrap/scss/print";
|
||||
//@import "~bootstrap/scss/scaffolding";
|
||||
@import "~bootstrap/scss/reboot";
|
||||
@import "~bootstrap/scss/type";
|
||||
@import "~bootstrap/scss/grid";
|
||||
@@ -19,12 +16,12 @@
|
||||
@import "~bootstrap/scss/list-group";
|
||||
@import "~bootstrap/scss/card";
|
||||
@import "~bootstrap/scss/utilities";
|
||||
//@import "~bootstrap/scss/responsive-utilities";
|
||||
|
||||
body {
|
||||
font-family: $font-family-sans-serif;
|
||||
|
||||
&.invoice_print {
|
||||
--bs-body-font-size: 13px;
|
||||
background-color: #eee;
|
||||
|
||||
.table.no-border, .table.no-border td, .table.no-border th {
|
||||
@@ -35,6 +32,38 @@ body {
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
.pt-1 {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.pb-4 {
|
||||
padding-bottom: 4em;
|
||||
}
|
||||
|
||||
.ps-0 {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.bt-1 {
|
||||
border-top: 1px solid #000000;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.text-end {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
margin: 105px auto 30px auto;
|
||||
padding: 50px 65px;
|
||||
@@ -48,7 +77,7 @@ body {
|
||||
|
||||
.page-header {
|
||||
margin: 10px 0 20px 0;
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
|
||||
> small {
|
||||
color: #666;
|
||||
@@ -79,14 +108,11 @@ body {
|
||||
}
|
||||
|
||||
.invoice-items {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 3em;
|
||||
|
||||
.table {
|
||||
thead th {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 15px
|
||||
padding-bottom: 10px
|
||||
}
|
||||
|
||||
tfoot {
|
||||
@@ -105,7 +131,6 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kimai/kimai",
|
||||
"license": "MIT",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "project",
|
||||
"description": "Kimai - Time Tracking",
|
||||
"authors": [
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
{
|
||||
"name": "All contributors",
|
||||
"homepage": "https://github.com/kevinpapst/kimai2/contributors"
|
||||
"homepage": "https://github.com/kimai/kimai/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"ext-zip": "*",
|
||||
"composer/package-versions-deprecated": "^1.8",
|
||||
"composer/semver": "^3.3",
|
||||
"doctrine/doctrine-bundle": "^2.0",
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.8",
|
||||
"endroid/qr-code": "^4.4.9",
|
||||
@@ -35,7 +35,7 @@
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jeroendesloovere/vcard": "^1.7",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^0.9",
|
||||
"kevinpapst/tabler-bundle": "^0.10",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
@@ -49,7 +49,6 @@
|
||||
"scheb/2fa-backup-code": "^6.2",
|
||||
"scheb/2fa-bundle": "^6.2",
|
||||
"scheb/2fa-totp": "^6.2",
|
||||
"sensio/framework-extra-bundle": "^6.0",
|
||||
"symfony/asset": "^6.0",
|
||||
"symfony/console": "^6.0",
|
||||
"symfony/dotenv": "^6.0",
|
||||
@@ -68,7 +67,7 @@
|
||||
"symfony/translation": "^6.0",
|
||||
"symfony/twig-bundle": "^6.0",
|
||||
"symfony/validator": "^6.0",
|
||||
"symfony/webpack-encore-bundle": "^1.5",
|
||||
"symfony/webpack-encore-bundle": "^1.16",
|
||||
"symfony/yaml": "^6.0",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
@@ -191,12 +190,12 @@
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"v2.x-dev": "2.0.x-dev"
|
||||
"dev-main": "2.0.x-dev"
|
||||
},
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "6.0.*"
|
||||
"require": "6.2.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1603
composer.lock
generated
@@ -5,7 +5,6 @@ return [
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
prefix_seed: "kimai"
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
#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
|
||||
|
||||
@@ -4,7 +4,7 @@ fos_rest:
|
||||
cache_dir: '%kernel.cache_dir%/fos_rest'
|
||||
routing_loader: false
|
||||
body_converter:
|
||||
enabled: true
|
||||
enabled: false
|
||||
serializer:
|
||||
serialize_null: true
|
||||
view:
|
||||
|
||||
@@ -19,16 +19,17 @@ framework:
|
||||
log: true
|
||||
|
||||
mailer:
|
||||
# prevent that emails are sent out in dev systems
|
||||
dsn: 'null://null'
|
||||
dsn: '%env(MAILER_URL)%'
|
||||
|
||||
when@prod:
|
||||
when@dev:
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_URL)%'
|
||||
dsn: 'null://null'
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
mailer:
|
||||
dsn: 'null://null'
|
||||
|
||||
@@ -29,14 +29,6 @@ when@dev:
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: info
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
||||
@@ -63,4 +63,4 @@ nelmio_api_doc:
|
||||
in: header
|
||||
security:
|
||||
- X-AUTH-USER: []
|
||||
- X-AUTH-TOKEN: []
|
||||
X-AUTH-TOKEN: []
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
strict_requirements: null
|
||||
|
||||
# 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
|
||||
|
||||
when@prod:
|
||||
when@test:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
strict_requirements: true
|
||||
|
||||
when@dev:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: true
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
security:
|
||||
enable_authenticator_manager: true
|
||||
|
||||
password_hashers:
|
||||
App\Entity\User: auto
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
sensio_framework_extra:
|
||||
router:
|
||||
annotations: false
|
||||
@@ -7,12 +7,7 @@ services:
|
||||
public: true
|
||||
|
||||
App\Configuration\SystemConfiguration:
|
||||
arguments: ['@App\Repository\ConfigurationRepository', "%kimai.config%"]
|
||||
|
||||
App\Repository\ConfigurationRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Configuration']
|
||||
arguments: ['@App\Configuration\ConfigurationService', "%kimai.config%"]
|
||||
|
||||
# required for the importer command test
|
||||
App\Repository\UserRepository:
|
||||
|
||||
@@ -11,8 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* All the checks for hasIndex() and hasColumn() are here to simplify testing and development,
|
||||
@@ -20,7 +20,7 @@ use Doctrine\Migrations\AbstractMigration;
|
||||
*
|
||||
* @version 2.0
|
||||
*/
|
||||
final class Version20993112235958 extends AbstractMigration
|
||||
final class Version20230126002049 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
682
phpstan.neon
@@ -570,316 +570,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Command/InvoiceCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Call to an undefined method object\\:\\:getEventManager\\(\\)\\.$#"
|
||||
count: 3
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getCustomers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getName\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
|
||||
count: 4
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getProjects\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getTimezone\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getUsers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasUsers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:countFromImport\\(\\) has parameter \\$where with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:countFromImport\\(\\) should return int but returns mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) should return App\\\\Entity\\\\Activity but returns App\\\\Entity\\\\Activity\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createInstanceTeam\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createInstanceTeam\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:deactivateLifecycleCallbacks\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchAllFromImport\\(\\) has parameter \\$where with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchAllFromImport\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchIteratorFromImport\\(\\) return type has no value type specified in iterable type Traversable\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importCustomers\\(\\) has parameter \\$customers with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$projects with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importTimesheetRecords\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importTimesheetRecords\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importUsers\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importUsers\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownActivity\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownCustomer\\(\\) has parameter \\$oldCustomer with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownGroup\\(\\) has parameter \\$oldGroup with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownProject\\(\\) has parameter \\$oldProject with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownUser\\(\\) has parameter \\$oldUser with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:prepareOptionsFromInput\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setActivityCache\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setCustomerCache\\(\\) has parameter \\$oldCustomer with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setGroupCache\\(\\) has parameter \\$oldGroup with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setProjectCache\\(\\) has parameter \\$oldProject with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setUserCache\\(\\) has parameter \\$oldUser with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$customer with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$projects with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$customer of method App\\\\Entity\\\\Team\\:\\:addCustomer\\(\\) expects App\\\\Entity\\\\Customer, App\\\\Entity\\\\Customer\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of method App\\\\Entity\\\\TimesheetMeta\\:\\:setName\\(\\) expects string, mixed given\\.$#"
|
||||
count: 3
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$object of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:persist\\(\\) expects object, App\\\\Entity\\\\Team\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Team\\:\\:addTeamlead\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Team\\:\\:addUser\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Timesheet\\:\\:setUser\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$object of method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateImport\\(\\) expects object, App\\\\Entity\\\\Team\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\Hasher\\\\UserPasswordHasherInterface\\:\\:hashPassword\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$team of method App\\\\Command\\\\KimaiImporterCommand\\:\\:setGroupCache\\(\\) expects App\\\\Entity\\\\Team, App\\\\Entity\\\\Team\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$version2 of function version_compare expects string, mixed given\\.$#"
|
||||
count: 2
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Command\\\\KimaiImporterCommand\\:\\:\\$oldActivities type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/KimaiImporterCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -1225,36 +915,16 @@ parameters:
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Offset \\(int\\|string\\) might not exist on array\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$array of function array_filter expects array, array\\|null given\\.$#"
|
||||
count: 2
|
||||
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 \\$key of method App\\\\Configuration\\\\SystemConfiguration\\:\\:set\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function trim expects string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, array\\|null given\\.$#"
|
||||
count: 3
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\AbstractController\\:\\:createFormForGetRequest\\(\\) has parameter \\$data with no type specified\\.$#"
|
||||
count: 1
|
||||
@@ -1300,21 +970,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ActivityController\\:\\:detailsAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ActivityController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ActivityController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -1385,71 +1040,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/Controller/CalendarController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:createAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:createDefaultTeamAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:deleteAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:deleteCommentAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:detailsAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:editAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:exportAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:pinCommentAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:projectsAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\CustomerController\\:\\:teamPermissionsAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$address of method JeroenDesloovere\\\\VCard\\\\VCard\\:\\:addEmail\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -1600,11 +1190,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/InvoiceController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\InvoiceController\\:\\:uploadDocumentAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/InvoiceController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
@@ -1650,86 +1235,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:activitiesAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createDefaultTeamAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createProject\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:createWithCustomerAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:deleteAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:deleteCommentAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:detailsAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:duplicateAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:editAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:exportAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:pinCommentAction\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\ProjectController\\:\\:teamPermissions\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -2090,11 +1595,6 @@ parameters:
|
||||
count: 3
|
||||
path: src/Controller/SystemConfigurationController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$model of method App\\\\Repository\\\\ConfigurationRepository\\:\\:saveSystemConfiguration\\(\\) expects App\\\\Form\\\\Model\\\\SystemConfiguration, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/SystemConfigurationController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$value of method App\\\\Form\\\\Model\\\\Configuration\\:\\:setValue\\(\\) expects bool\\|int\\|object\\|string\\|null, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
@@ -2290,11 +1790,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/UserController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\UserController\\:\\:indexAction\\(\\) has parameter \\$page with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/UserController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\UserExporter\\:\\:export\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
|
||||
count: 1
|
||||
@@ -2385,16 +1880,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/DataFixtures/TeamFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 1 on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 2 on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method setTimestamp\\(\\) on DateTime\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -2405,51 +1890,21 @@ parameters:
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:createTimesheetEntry\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:findRandom\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$begin of method App\\\\Entity\\\\Timesheet\\:\\:setBegin\\(\\) expects DateTime, DateTime\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$className of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\) expects class\\-string\\<object\\>, string given\\.$#"
|
||||
count: 2
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$description of method App\\\\Entity\\\\Timesheet\\:\\:setDescription\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$min of function rand expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:createTimesheetEntry\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$max of function rand expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Unable to resolve the template type T in call to method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\)$#"
|
||||
count: 2
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:getUserDefinition\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -2460,16 +1915,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/DataFixtures/UserFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:loadDefaultAccounts\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/UserFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:loadTestUsers\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/DataFixtures/UserFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:createPermissionParameter\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -4200,11 +3645,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/Helper/ProjectHelper.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\InvoiceDocumentUploadForm\\:\\:validateDocument\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Form/InvoiceDocumentUploadForm.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\InvoiceDocumentUploadForm\\:\\:validateDocument\\(\\) has parameter \\$value with no type specified\\.$#"
|
||||
count: 1
|
||||
@@ -6761,7 +6201,7 @@ parameters:
|
||||
path: src/Project/ProjectStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Strict comparison using \\!\\=\\= between null and array\\<array\\{id\\: int\\|numeric\\-string, duration\\: int\\|numeric\\-string, rate\\: float\\|int\\|numeric\\-string, internalRate\\: float\\|int\\|numeric\\-string, counter\\: int\\<0, max\\>\\|numeric\\-string, billable\\: bool, exported\\: bool\\}\\> will always evaluate to true\\.$#"
|
||||
message: "#^Strict comparison using \\!\\=\\= between null and list\\<array\\{id\\: int\\|numeric\\-string, duration\\: int\\|numeric\\-string, rate\\: float\\|int\\|numeric\\-string, internalRate\\: float\\|int\\|numeric\\-string, counter\\: int\\<0, max\\>\\|numeric\\-string, billable\\: bool, exported\\: bool\\}\\> will always evaluate to true\\.$#"
|
||||
count: 1
|
||||
path: src/Project/ProjectStatisticService.php
|
||||
|
||||
@@ -6895,11 +6335,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/BookmarkRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ConfigurationRepository\\:\\:saveSystemConfiguration\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ConfigurationRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -6970,31 +6405,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:__construct\\(\\) has parameter \\$directories with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceDocumentRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:addDirectory\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceDocumentRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:findByPaths\\(\\) has parameter \\$paths with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceDocumentRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:removeDirectory\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceDocumentRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$filename of function unlink expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceDocumentRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'counter' on mixed\\.$#"
|
||||
count: 1
|
||||
@@ -7135,11 +6545,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/Paginator/LoaderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Paginator\\\\LoaderPaginator\\:\\:getResults\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/LoaderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\) expects array, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -7165,11 +6570,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/Paginator/QueryBuilderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Paginator\\\\QueryBuilderPaginator\\:\\:getResults\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/QueryBuilderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -8625,76 +8025,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Validator/Constraints/UserValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Activity\\) of method App\\\\Voter\\\\ActivityVoter\\:\\:supports\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:supports\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/ActivityVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Activity\\) of method App\\\\Voter\\\\ActivityVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/ActivityVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Customer\\) of method App\\\\Voter\\\\CustomerVoter\\:\\:supports\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:supports\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/CustomerVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Customer\\) of method App\\\\Voter\\\\CustomerVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/CustomerVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Activity\\|App\\\\Entity\\\\Customer\\|App\\\\Entity\\\\Project\\|string\\) of method App\\\\Voter\\\\EntityMultiRoleVoter\\:\\:supports\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:supports\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/EntityMultiRoleVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Activity\\|App\\\\Entity\\\\Customer\\|App\\\\Entity\\\\Project\\|string\\) of method App\\\\Voter\\\\EntityMultiRoleVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/EntityMultiRoleVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Project\\) of method App\\\\Voter\\\\ProjectVoter\\:\\:supports\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:supports\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/ProjectVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Project\\) of method App\\\\Voter\\\\ProjectVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/ProjectVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\User\\) of method App\\\\Voter\\\\QuickEntryVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/QuickEntryVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(null\\) of method App\\\\Voter\\\\ReportingVoter\\:\\:supports\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:supports\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/ReportingVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(null\\) of method App\\\\Voter\\\\ReportingVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/ReportingVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Activity\\) of method App\\\\Voter\\\\RolePermissionVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/RolePermissionVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Team\\) of method App\\\\Voter\\\\TeamVoter\\:\\:supports\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:supports\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/TeamVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Team\\) of method App\\\\Voter\\\\TeamVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/TeamVoter.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -8705,16 +8035,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Voter/TimesheetVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Voter\\\\TimesheetVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/TimesheetVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject \\(App\\\\Entity\\\\User\\) of method App\\\\Voter\\\\UserVoter\\:\\:voteOnAttribute\\(\\) should be contravariant with parameter \\$subject \\(mixed\\) of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authorization\\\\Voter\\\\Voter\\:\\:voteOnAttribute\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Voter/UserVoter.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'billable' on mixed\\.$#"
|
||||
count: 1
|
||||
|
||||
BIN
public/apple-touch-icon-precomposed.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 30 KiB |
@@ -2,9 +2,7 @@
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="favicon/mstile-small.jpg"/>
|
||||
<square150x150logo src="favicon/mstile-medium.jpg"/>
|
||||
<wide310x150logo src="favicon/mstile-wide.jpg"/>
|
||||
<square150x150logo src="favicon/mstile-150x150.png"/>
|
||||
<square310x310logo src="favicon/mstile-large.jpg"/>
|
||||
<TileColor>#00a300</TileColor>
|
||||
</tile>
|
||||
|
||||
2
public/build/app.8b0c21dc.js
Normal file
@@ -3,7 +3,7 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/app.694c6cb5.js"
|
||||
"/build/app.8b0c21dc.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.73c27235.css"
|
||||
@@ -24,7 +24,7 @@
|
||||
"/build/invoice.c8ae95ad.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/invoice.b17784c1.css"
|
||||
"/build/invoice.3c80ee80.css"
|
||||
]
|
||||
},
|
||||
"invoice-pdf": {
|
||||
@@ -45,7 +45,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/calendar.414914c7.js"
|
||||
"/build/calendar.7dd54a5f.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.bb473428.css"
|
||||
@@ -63,16 +63,16 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||
"/build/app.694c6cb5.js": "sha384-RJ7fX6EM951Vhf6E63ZqGb9G+9fFVvC3V8xZt4CZDSk6TUFs7ITkLwwiKoj1xlAW",
|
||||
"/build/app.8b0c21dc.js": "sha384-k0LUmYHZUK6bFVymGMzcHSq+utW09iR+YG6VMzqCiXfBGtVvb2Sdr0IB7w6hR+I6",
|
||||
"/build/app.73c27235.css": "sha384-0vdzXrL3JuXTfiYJUdY17wIGFIbUTzQWfBQXbhcXcFRVmpmJYY1TZYSauxVUNwev",
|
||||
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",
|
||||
"/build/invoice.b17784c1.css": "sha384-Y1e218/TMmOrl/aQcb77ix5qgFQDPPWpeD6GUtnX16Buj7/Mr6arWMSXFdqlJzAc",
|
||||
"/build/invoice.3c80ee80.css": "sha384-xjFM2m/EeN7z42ygpt77ll5zAcHTeOjbZFAw2Qcu3IJutgebiAbrXjnH5aJfF5Z6",
|
||||
"/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq",
|
||||
"/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv",
|
||||
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
||||
"/build/calendar.414914c7.js": "sha384-v0La1MlAfoR6x87S4ucwp70e6ma0Y8uIf77i9ADTq/xB+D0Sn3/L67XQDXW1bwY6",
|
||||
"/build/calendar.7dd54a5f.js": "sha384-oUWTIvkHr+k1UrlriT4+iA+irValKNLFoxva7ljQ/bsbczzaql3uWD0dlC4mF0ss",
|
||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"build/app.css": "/build/app.73c27235.css",
|
||||
"build/app.js": "/build/app.694c6cb5.js",
|
||||
"build/app.js": "/build/app.8b0c21dc.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
||||
"build/invoice.css": "/build/invoice.b17784c1.css",
|
||||
"build/invoice.css": "/build/invoice.3c80ee80.css",
|
||||
"build/invoice.js": "/build/invoice.c8ae95ad.js",
|
||||
"build/invoice-pdf.css": "/build/invoice-pdf.c88953bb.css",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js",
|
||||
"build/chart.js": "/build/chart.f5becfac.js",
|
||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||
"build/calendar.js": "/build/calendar.414914c7.js",
|
||||
"build/calendar.js": "/build/calendar.7dd54a5f.js",
|
||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
||||
"build/runtime.js": "/build/runtime.f0079159.js"
|
||||
|
||||
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/favicon/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,21 +1,31 @@
|
||||
{
|
||||
"name": "Kimai Time-Tracker",
|
||||
"short_name": "Kimai 2",
|
||||
"short_name": "Kimai",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png",
|
||||
"density": "0.75"
|
||||
"src": "/apple-touch-icon.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "touch-icon-192x192.png",
|
||||
"src": "/touch-icon-180x180.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/touch-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "4.0"
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/touch-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"theme-color": "#1d273b",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -1,5 +1,2 @@
|
||||
# www.robotstxt.org/
|
||||
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
|
||||
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
|
||||
BIN
public/touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/touch-icon-512x512.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
@@ -21,14 +21,14 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/actions')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Actions')]
|
||||
final class ActionsController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -26,15 +26,15 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/activities')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Activity')]
|
||||
final class ActivityController extends BaseApiController
|
||||
{
|
||||
@@ -179,7 +179,7 @@ final class ActivityController extends BaseApiController
|
||||
/**
|
||||
* Update an existing activity
|
||||
*/
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
#[OA\Patch(description: 'Update an existing activity, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated activity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityEditForm'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to update', required: true)]
|
||||
@@ -217,7 +217,7 @@ final class ActivityController extends BaseApiController
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing activity
|
||||
*/
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Activity record ID to set the meta-field value for', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]
|
||||
@@ -250,7 +250,7 @@ final class ActivityController extends BaseApiController
|
||||
/**
|
||||
* Returns a collection of all rates for one activity
|
||||
*/
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of activity rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityRate')))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The activity whose rates will be returned', required: true)]
|
||||
#[Rest\Get(path: '/{id}/rates', name: 'get_activity_rates', requirements: ['id' => '\d+'])]
|
||||
@@ -269,15 +269,14 @@ final class ActivityController extends BaseApiController
|
||||
/**
|
||||
* Deletes one rate for an activity
|
||||
*/
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The activity whose rate will be removed', required: true)]
|
||||
#[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_activity_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])]
|
||||
#[Entity('rate', expr: 'repository.find(rateId)')]
|
||||
public function deleteRateAction(Activity $activity, ActivityRate $rate): Response
|
||||
public function deleteRateAction(Activity $activity, #[MapEntity(mapping: ['rateId' => 'id'])] ActivityRate $rate): Response
|
||||
{
|
||||
if ($rate->getActivity() !== $activity) {
|
||||
throw $this->createNotFoundException();
|
||||
@@ -293,7 +292,7 @@ final class ActivityController extends BaseApiController
|
||||
/**
|
||||
* Adds a new rate to an activity
|
||||
*/
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ActivityRate'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The activity to add the rate for', required: true)]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityRateForm'))]
|
||||
|
||||
@@ -9,12 +9,17 @@
|
||||
|
||||
namespace App\API\Authentication;
|
||||
|
||||
use App\Entity\User;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @param string|null $plaintextApiToken
|
||||
* @param PasswordUpgraderInterface<User> $passwordUpgrader
|
||||
*/
|
||||
public function __construct(private ?string $plaintextApiToken, private PasswordUpgraderInterface $passwordUpgrader)
|
||||
{
|
||||
}
|
||||
@@ -31,6 +36,9 @@ final class ApiTokenUpgradeBadge implements BadgeInterface
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PasswordUpgraderInterface<User>
|
||||
*/
|
||||
public function getPasswordUpgrader(): PasswordUpgraderInterface
|
||||
{
|
||||
return $this->passwordUpgrader;
|
||||
|
||||
@@ -17,10 +17,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Default')]
|
||||
final class ConfigurationController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -26,15 +26,15 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/customers')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Customer')]
|
||||
final class CustomerController extends BaseApiController
|
||||
{
|
||||
@@ -156,7 +156,7 @@ final class CustomerController extends BaseApiController
|
||||
/**
|
||||
* Update an existing customer
|
||||
*/
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
#[OA\Patch(description: 'Update an existing customer, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated customer', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerEditForm'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Customer ID to update', required: true)]
|
||||
@@ -194,7 +194,7 @@ final class CustomerController extends BaseApiController
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing customer
|
||||
*/
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Customer record ID to set the meta-field value for', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]
|
||||
@@ -227,7 +227,7 @@ final class CustomerController extends BaseApiController
|
||||
/**
|
||||
* Returns a collection of all rates for one customer
|
||||
*/
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of customer rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/CustomerRate')))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The customer whose rates will be returned', required: true)]
|
||||
#[Rest\Get(path: '/{id}/rates', name: 'get_customer_rates', requirements: ['id' => '\d+'])]
|
||||
@@ -246,15 +246,14 @@ final class CustomerController extends BaseApiController
|
||||
/**
|
||||
* Deletes one rate for a customer
|
||||
*/
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The customer whose rate will be removed', required: true)]
|
||||
#[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_customer_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])]
|
||||
#[Entity('rate', expr: 'repository.find(rateId)')]
|
||||
public function deleteRateAction(Customer $customer, CustomerRate $rate): Response
|
||||
public function deleteRateAction(Customer $customer, #[MapEntity(mapping: ['rateId' => 'id'])] CustomerRate $rate): Response
|
||||
{
|
||||
if ($rate->getCustomer() !== $customer) {
|
||||
throw $this->createNotFoundException();
|
||||
@@ -270,7 +269,7 @@ final class CustomerController extends BaseApiController
|
||||
/**
|
||||
* Adds a new rate to a customer
|
||||
*/
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/CustomerRate'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The customer to add the rate for', required: true)]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerRateForm'))]
|
||||
|
||||
@@ -27,16 +27,16 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
|
||||
#[Route(path: '/projects')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Project')]
|
||||
final class ProjectController extends BaseApiController
|
||||
{
|
||||
@@ -209,7 +209,7 @@ final class ProjectController extends BaseApiController
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
#[Security("is_granted('edit', project)")]
|
||||
#[IsGranted('edit', 'project')]
|
||||
#[OA\Patch(description: 'Update an existing project, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated project', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectEditForm'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Project ID to update', required: true)]
|
||||
@@ -249,7 +249,7 @@ final class ProjectController extends BaseApiController
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing project
|
||||
*/
|
||||
#[Security("is_granted('edit', project)")]
|
||||
#[IsGranted('edit', 'project')]
|
||||
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Project record ID to set the meta-field value for', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]
|
||||
@@ -282,7 +282,7 @@ final class ProjectController extends BaseApiController
|
||||
/**
|
||||
* Returns a collection of all rates for one project
|
||||
*/
|
||||
#[Security("is_granted('edit', project)")]
|
||||
#[IsGranted('edit', 'project')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of project rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ProjectRate')))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The project whose rates will be returned', required: true)]
|
||||
#[Rest\Get(path: '/{id}/rates', name: 'get_project_rates', requirements: ['id' => '\d+'])]
|
||||
@@ -301,15 +301,14 @@ final class ProjectController extends BaseApiController
|
||||
/**
|
||||
* Deletes one rate for a project
|
||||
*/
|
||||
#[Security("is_granted('edit', project)")]
|
||||
#[IsGranted('edit', 'project')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The project whose rate will be removed', required: true)]
|
||||
#[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_project_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])]
|
||||
#[Entity('rate', expr: 'repository.find(rateId)')]
|
||||
public function deleteRateAction(Project $project, ProjectRate $rate): Response
|
||||
public function deleteRateAction(Project $project, #[MapEntity(mapping: ['rateId' => 'id'])] ProjectRate $rate): Response
|
||||
{
|
||||
if ($rate->getProject() !== $project) {
|
||||
throw $this->createNotFoundException();
|
||||
@@ -325,7 +324,7 @@ final class ProjectController extends BaseApiController
|
||||
/**
|
||||
* Adds a new rate to a project
|
||||
*/
|
||||
#[Security("is_granted('edit', project)")]
|
||||
#[IsGranted('edit', 'project')]
|
||||
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ProjectRate'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The project to add the rate for', required: true)]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectRateForm'))]
|
||||
|
||||
@@ -16,8 +16,8 @@ use JMS\Serializer\Context;
|
||||
use JMS\Serializer\GraphNavigatorInterface;
|
||||
use JMS\Serializer\Handler\SubscribingHandlerInterface;
|
||||
use JMS\Serializer\JsonSerializationVisitor;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\ErrorHandler\Exception\FlattenException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Default')]
|
||||
final class StatusController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -18,13 +18,13 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/tags')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Tag')]
|
||||
final class TagController extends BaseApiController
|
||||
{
|
||||
@@ -94,7 +94,7 @@ final class TagController extends BaseApiController
|
||||
/**
|
||||
* Delete a tag
|
||||
*/
|
||||
#[Security("is_granted('delete_tag')")]
|
||||
#[IsGranted('delete_tag')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'HTTP code 204 for a successful delete')])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Tag ID to delete', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
|
||||
@@ -21,15 +21,15 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/teams')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Team')]
|
||||
final class TeamController extends BaseApiController
|
||||
{
|
||||
@@ -44,7 +44,7 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Fetch all existing teams (which are visible to the user)
|
||||
*/
|
||||
#[Security("is_granted('view_team')")]
|
||||
#[IsGranted('view_team')]
|
||||
#[OA\Response(response: 200, description: 'Returns the collection of teams', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamCollection')))]
|
||||
#[Rest\Get(path: '', name: 'get_teams')]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -62,7 +62,7 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Returns one team
|
||||
*/
|
||||
#[Security("is_granted('view_team')")]
|
||||
#[IsGranted('view_team')]
|
||||
#[OA\Response(response: 200, description: 'Returns one team entity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))]
|
||||
#[Rest\Get(path: '/{id}', name: 'get_team', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -78,7 +78,7 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Delete a team
|
||||
*/
|
||||
#[Security("is_granted('delete_team')")]
|
||||
#[IsGranted('delete_team')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one team')])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to delete', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -96,7 +96,7 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Creates a new team
|
||||
*/
|
||||
#[Security("is_granted('create_team')")]
|
||||
#[IsGranted('create_team')]
|
||||
#[OA\Post(description: 'Creates a new team and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))]
|
||||
#[Rest\Post(path: '', name: 'post_team')]
|
||||
@@ -127,7 +127,7 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Update an existing team
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Patch(description: 'Update an existing team, you can pass all or just a subset of all attributes (passing members will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to update', required: true)]
|
||||
@@ -167,15 +167,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Add a new member to a team
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new user to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team which will receive the new member', required: true)]
|
||||
#[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to add (User ID)', required: true)]
|
||||
#[Rest\Post(path: '/{id}/members/{userId}', name: 'post_team_member', requirements: ['id' => '\d+', 'userId' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Entity('member', expr: 'repository.find(userId)')]
|
||||
public function postMemberAction(Team $team, User $member): Response
|
||||
public function postMemberAction(Team $team, #[MapEntity(mapping: ['userId' => 'id'])] User $member): Response
|
||||
{
|
||||
if ($member->isInTeam($team)) {
|
||||
throw new BadRequestHttpException('User is already member of the team');
|
||||
@@ -194,15 +193,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Removes a member from the team
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a user from the team. The teamlead cannot be removed.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team from which the member will be removed', required: true)]
|
||||
#[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to remove (User ID)', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/members/{userId}', name: 'delete_team_member', requirements: ['id' => '\d+', 'userId' => '\d+'])]
|
||||
#[Entity('member', expr: 'repository.find(userId)')]
|
||||
public function deleteMemberAction(Team $team, User $member): Response
|
||||
public function deleteMemberAction(Team $team, #[MapEntity(mapping: ['userId' => 'id'])] User $member): Response
|
||||
{
|
||||
if (!$member->isInTeam($team)) {
|
||||
throw new BadRequestHttpException('User is not a member of the team');
|
||||
@@ -225,15 +223,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Grant the team access to a customer
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new customer to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)]
|
||||
#[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to grant acecess to (Customer ID)', required: true)]
|
||||
#[Rest\Post(path: '/{id}/customers/{customerId}', name: 'post_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Entity('customer', expr: 'repository.find(customerId)')]
|
||||
public function postCustomerAction(Team $team, Customer $customer): Response
|
||||
public function postCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
|
||||
{
|
||||
if ($team->hasCustomer($customer)) {
|
||||
throw new BadRequestHttpException('Team has already access to customer');
|
||||
@@ -252,15 +249,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Revokes access for a customer from a team
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a customer from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)]
|
||||
#[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to remove (Customer ID)', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/customers/{customerId}', name: 'delete_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])]
|
||||
#[Entity('customer', expr: 'repository.find(customerId)')]
|
||||
public function deleteCustomerAction(Team $team, Customer $customer): Response
|
||||
public function deleteCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
|
||||
{
|
||||
if (!$team->hasCustomer($customer)) {
|
||||
throw new BadRequestHttpException('Customer is not assigned to the team');
|
||||
@@ -279,15 +275,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Grant the team access to a project
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new project to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)]
|
||||
#[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to grant acecess to (Project ID)', required: true)]
|
||||
#[Rest\Post(path: '/{id}/projects/{projectId}', name: 'post_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Entity('project', expr: 'repository.find(projectId)')]
|
||||
public function postProjectAction(Team $team, Project $project): Response
|
||||
public function postProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
|
||||
{
|
||||
if ($team->hasProject($project)) {
|
||||
throw new BadRequestHttpException('Team has already access to project');
|
||||
@@ -306,15 +301,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Revokes access for a project from a team
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a project from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)]
|
||||
#[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to remove (Project ID)', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/projects/{projectId}', name: 'delete_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])]
|
||||
#[Entity('project', expr: 'repository.find(projectId)')]
|
||||
public function deleteProjectAction(Team $team, Project $project): Response
|
||||
public function deleteProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
|
||||
{
|
||||
if (!$team->hasProject($project)) {
|
||||
throw new BadRequestHttpException('Project is not assigned to the team');
|
||||
@@ -333,15 +327,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Grant the team access to an activity
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new activity to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)]
|
||||
#[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to grant acecess to (Activity ID)', required: true)]
|
||||
#[Rest\Post(path: '/{id}/activities/{activityId}', name: 'post_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Entity('activity', expr: 'repository.find(activityId)')]
|
||||
public function postActivityAction(Team $team, Activity $activity): Response
|
||||
public function postActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
|
||||
{
|
||||
if ($team->hasActivity($activity)) {
|
||||
throw new BadRequestHttpException('Team has already access to activity');
|
||||
@@ -360,15 +353,14 @@ final class TeamController extends BaseApiController
|
||||
/**
|
||||
* Revokes access for an activity from a team
|
||||
*/
|
||||
#[Security("is_granted('edit_team')")]
|
||||
#[IsGranted('edit_team')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a activity from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)]
|
||||
#[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to remove (Activity ID)', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/activities/{activityId}', name: 'delete_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])]
|
||||
#[Entity('activity', expr: 'repository.find(activityId)')]
|
||||
public function deleteActivityAction(Team $team, Activity $activity): Response
|
||||
public function deleteActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
|
||||
{
|
||||
if (!$team->hasActivity($activity)) {
|
||||
throw new BadRequestHttpException('Activity is not assigned to the team');
|
||||
|
||||
@@ -33,16 +33,17 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[Route(path: '/timesheets')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'Timesheet')]
|
||||
final class TimesheetController extends BaseApiController
|
||||
{
|
||||
@@ -69,7 +70,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Returns a collection of timesheet records (which are visible to the user)
|
||||
*/
|
||||
#[Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')")]
|
||||
#[IsGranted(new Expression("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')"))]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of timesheet records. The datetime fields are given in the users local time including the timezone offset (ISO-8601).', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollection')))]
|
||||
#[Rest\Get(path: '', name: 'get_timesheets')]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -256,7 +257,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Returns one timesheet record
|
||||
*/
|
||||
#[Security("is_granted('view', timesheet)")]
|
||||
#[IsGranted('view', 'timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Returns one timesheet record. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to fetch', required: true)]
|
||||
#[Rest\Get(path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])]
|
||||
@@ -273,7 +274,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Creates a new timesheet record
|
||||
*/
|
||||
#[Security("is_granted('create_own_timesheet')")]
|
||||
#[IsGranted('create_own_timesheet')]
|
||||
#[OA\Post(description: 'Creates a new timesheet record for the current user and returns it afterwards.', responses: [new OA\Response(response: 200, description: 'Returns the new created timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
|
||||
#[Rest\Post(path: '', name: 'post_timesheet')]
|
||||
@@ -328,7 +329,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Update an existing timesheet record
|
||||
*/
|
||||
#[Security("is_granted('edit', timesheet)")]
|
||||
#[IsGranted('edit', 'timesheet')]
|
||||
#[OA\Patch(description: 'Update an existing timesheet record, you can pass all or just a subset of the attributes.', responses: [new OA\Response(response: 200, description: 'Returns the updated timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to update', required: true)]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
|
||||
@@ -373,7 +374,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Delete an existing timesheet record
|
||||
*/
|
||||
#[Security("is_granted('delete', timesheet)")]
|
||||
#[IsGranted('delete', 'timesheet')]
|
||||
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet record')])]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to delete', required: true)]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -391,7 +392,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Returns the collection of recent user activities
|
||||
*/
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Returns the 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')))]
|
||||
#[Rest\Get(path: '/recent', name: 'recent_timesheet')]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -427,7 +428,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Returns the collection of active timesheet records
|
||||
*/
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Returns the collection of active timesheet records for the current user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
|
||||
#[Rest\Get(path: '/active', name: 'active_timesheet')]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -448,7 +449,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Stops an active timesheet record
|
||||
*/
|
||||
#[Security("is_granted('stop', timesheet)")]
|
||||
#[IsGranted('stop', 'timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Stops an active timesheet record and returns it afterwards.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to stop', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/stop', name: 'stop_timesheet', requirements: ['id' => '\d+'])]
|
||||
@@ -467,7 +468,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Restarts a previously stopped timesheet record for the current user
|
||||
*/
|
||||
#[Security("is_granted('start', timesheet)")]
|
||||
#[IsGranted('start', 'timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])]
|
||||
@@ -529,7 +530,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Duplicates an existing timesheet record
|
||||
*/
|
||||
#[Security("is_granted('duplicate', timesheet)")]
|
||||
#[IsGranted('duplicate', 'timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Duplicates a timesheet record, resetting the export state only.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to duplicate', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/duplicate', name: 'duplicate_timesheet', requirements: ['id' => '\d+'])]
|
||||
@@ -552,7 +553,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Switch the export state of a timesheet record to (un-)lock it
|
||||
*/
|
||||
#[Security("is_granted('edit_export', timesheet)")]
|
||||
#[IsGranted('edit_export', 'timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Switches the exported state on the record and therefor locks / unlocks it for further updates. Needs edit_export_*_timesheet permission.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to switch export state', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/export', name: 'export_timesheet', requirements: ['id' => '\d+'])]
|
||||
@@ -577,7 +578,7 @@ final class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing timesheet.
|
||||
*/
|
||||
#[Security("is_granted('edit', timesheet)")]
|
||||
#[IsGranted('edit', 'timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to set the meta-field value for', required: true)]
|
||||
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]
|
||||
|
||||
@@ -23,16 +23,16 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/users')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[OA\Tag(name: 'User')]
|
||||
final class UserController extends BaseApiController
|
||||
{
|
||||
@@ -51,7 +51,7 @@ final class UserController extends BaseApiController
|
||||
/**
|
||||
* Returns the collection of users (which are visible to the user)
|
||||
*/
|
||||
#[Security("is_granted('view_user')")]
|
||||
#[IsGranted('view_user')]
|
||||
#[OA\Response(response: 200, description: 'Returns the collection of users. Required permission: view_user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserCollection')))]
|
||||
#[Rest\Get(path: '', name: 'get_users')]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
@@ -95,7 +95,7 @@ final class UserController extends BaseApiController
|
||||
/**
|
||||
* Return one user entity
|
||||
*/
|
||||
#[Security("is_granted('view', profile)")]
|
||||
#[IsGranted('view', 'profile')]
|
||||
#[OA\Response(response: 200, description: 'Return one user entity.', content: new OA\JsonContent(ref: '#/components/schemas/UserEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'User ID to fetch', required: true)]
|
||||
#[Rest\Get(path: '/{id}', name: 'get_user', requirements: ['id' => '\d+'])]
|
||||
@@ -131,7 +131,7 @@ final class UserController extends BaseApiController
|
||||
/**
|
||||
* Creates a new user
|
||||
*/
|
||||
#[Security("is_granted('create_user')")]
|
||||
#[IsGranted('create_user')]
|
||||
#[OA\Post(description: 'Creates a new user and returns it afterwards')]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserCreateForm'))]
|
||||
#[Rest\Post(path: '', name: 'post_user')]
|
||||
@@ -184,7 +184,7 @@ final class UserController extends BaseApiController
|
||||
/**
|
||||
* Update an existing user
|
||||
*/
|
||||
#[Security("is_granted('edit', profile)")]
|
||||
#[IsGranted('edit', 'profile')]
|
||||
#[OA\Patch(description: 'Update an existing user, you can pass all or just a subset of all attributes (passing roles will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated user', content: new OA\JsonContent(ref: '#/components/schemas/UserEntity'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserEditForm'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'User ID to update', required: true)]
|
||||
|
||||
@@ -9,18 +9,13 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
use App\Entity\Configuration;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface ConfigLoaderInterface
|
||||
{
|
||||
/**
|
||||
* @param string $name
|
||||
* @return ?Configuration
|
||||
*/
|
||||
public function getConfiguration(string $name): ?Configuration;
|
||||
|
||||
/**
|
||||
* @return Configuration[]
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
public function getConfigurations(): array;
|
||||
}
|
||||
|
||||
72
src/Configuration/ConfigurationService.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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\Configuration;
|
||||
|
||||
use App\Entity\Configuration;
|
||||
use App\Form\Model\SystemConfiguration;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
final class ConfigurationService implements ConfigLoaderInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, string|null>
|
||||
*/
|
||||
private static array $cacheAll = [];
|
||||
private static bool $initialized = false;
|
||||
|
||||
public function __construct(private ConfigurationRepository $configurationRepository, private CacheInterface $cache)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
public function getConfigurations(): array
|
||||
{
|
||||
if (self::$initialized === true) {
|
||||
return self::$cacheAll;
|
||||
}
|
||||
|
||||
self::$cacheAll = $this->cache->get('configurations', function (ItemInterface $item) {
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
return $this->configurationRepository->getConfigurations();
|
||||
});
|
||||
|
||||
self::$initialized = true;
|
||||
|
||||
return self::$cacheAll;
|
||||
}
|
||||
|
||||
public function getConfiguration(string $name): ?Configuration
|
||||
{
|
||||
return $this->configurationRepository->findOneBy(['name' => $name]);
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cache->delete('configurations');
|
||||
self::$initialized = false;
|
||||
}
|
||||
|
||||
public function saveConfiguration(Configuration $configuration): void
|
||||
{
|
||||
$this->configurationRepository->saveConfiguration($configuration);
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
public function saveSystemConfiguration(SystemConfiguration $model): void
|
||||
{
|
||||
$this->configurationRepository->saveSystemConfiguration($model);
|
||||
$this->clearCache();
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
final class SamlConfiguration implements SamlConfigurationInterface
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
@@ -35,22 +32,17 @@ final class SamlConfiguration implements SamlConfigurationInterface
|
||||
|
||||
public function getAttributeMapping(): array
|
||||
{
|
||||
return $this->configuration->findArray('saml.mapping');
|
||||
return $this->configuration->getSamlAttributeMapping();
|
||||
}
|
||||
|
||||
public function getRolesAttribute(): ?string
|
||||
{
|
||||
$attr = $this->configuration->find('saml.roles.attribute');
|
||||
if (empty($attr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $attr;
|
||||
return $this->configuration->getSamlRolesAttribute();
|
||||
}
|
||||
|
||||
public function getRolesMapping(): array
|
||||
{
|
||||
return $this->configuration->findArray('saml.roles.mapping');
|
||||
return $this->configuration->getSamlRolesMapping();
|
||||
}
|
||||
|
||||
public function isRolesResetOnLogin(): bool
|
||||
@@ -60,6 +52,6 @@ final class SamlConfiguration implements SamlConfigurationInterface
|
||||
|
||||
public function getConnection(): array
|
||||
{
|
||||
return $this->configuration->findArray('saml.connection');
|
||||
return $this->configuration->getSamlConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,32 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
interface SamlConfigurationInterface
|
||||
{
|
||||
/**
|
||||
* Whether SAML login is activated.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isActivated(): bool;
|
||||
|
||||
/**
|
||||
* Returns the title that is exclusively used in the frontend.
|
||||
* Currently, used to display the button in the login screen.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* Returns the provider name that is exclusively used in the frontend.
|
||||
* Currently, used to display an icon in the login screen.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProvider(): string;
|
||||
|
||||
public function getAttributeMapping(): array;
|
||||
|
||||
@@ -13,7 +13,7 @@ final class SystemConfiguration
|
||||
{
|
||||
private bool $initialized = false;
|
||||
|
||||
public function __construct(private ConfigLoaderInterface $repository, private ?array $settings)
|
||||
public function __construct(private ConfigLoaderInterface $repository, private array $settings = [])
|
||||
{
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ final class SystemConfiguration
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->repository->getConfigurations() as $configuration) {
|
||||
$this->set($configuration->getName(), $configuration->getValue());
|
||||
foreach ($this->repository->getConfigurations() as $key => $value) {
|
||||
$this->set($key, $value);
|
||||
}
|
||||
|
||||
$this->initialized = true;
|
||||
@@ -35,7 +35,6 @@ final class SystemConfiguration
|
||||
*
|
||||
* If no key is given to the method, the entire array will be replaced.
|
||||
*
|
||||
* @see https://github.com/divineomega/array_undot
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
@@ -70,6 +69,7 @@ final class SystemConfiguration
|
||||
/**
|
||||
* This method should be avoided if possible, use plain keys instead.
|
||||
*
|
||||
* @see https://github.com/divineomega/array_undot
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
@@ -218,6 +218,40 @@ final class SystemConfiguration
|
||||
return (bool) $this->find('saml.roles.resetOnLogin');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<'saml'|'kimai', string>>
|
||||
*/
|
||||
public function getSamlRolesMapping(): array
|
||||
{
|
||||
return $this->findArray('saml.roles.mapping');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<mixed>|bool>
|
||||
*/
|
||||
public function getSamlConnection(): array
|
||||
{
|
||||
return $this->findArray('saml.connection');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<'saml'|'kimai', string>>
|
||||
*/
|
||||
public function getSamlAttributeMapping(): array
|
||||
{
|
||||
return $this->findArray('saml.mapping');
|
||||
}
|
||||
|
||||
public function getSamlRolesAttribute(): ?string
|
||||
{
|
||||
$attr = $this->find('saml.roles.attribute');
|
||||
if (empty($attr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $attr;
|
||||
}
|
||||
|
||||
public function isLdapActive(): bool
|
||||
{
|
||||
return (bool) $this->find('ldap.activate');
|
||||
|
||||
@@ -35,18 +35,19 @@ use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to manage activities.
|
||||
*/
|
||||
#[Route(path: '/admin/activity')]
|
||||
#[Security("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')")]
|
||||
#[IsGranted(new Expression("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')"))]
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
|
||||
@@ -55,7 +56,7 @@ final class ActivityController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_activity', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_activity_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request)
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -122,8 +123,8 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/details', name: 'activity_details', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', activity)")]
|
||||
public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository, ActivityStatisticService $statisticService)
|
||||
#[IsGranted('view', 'activity')]
|
||||
public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository, ActivityStatisticService $statisticService): Response
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -172,14 +173,14 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate/{rate}', name: 'admin_activity_rate_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
public function editRateAction(Activity $activity, ActivityRate $rate, Request $request, ActivityRateRepository $repository): Response
|
||||
{
|
||||
return $this->rateFormAction($activity, $rate, $request, $repository, $this->generateUrl('admin_activity_rate_edit', ['id' => $activity->getId(), 'rate' => $rate->getId()]));
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate', name: 'admin_activity_rate_add', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository): Response
|
||||
{
|
||||
$rate = new ActivityRate();
|
||||
@@ -216,14 +217,14 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/create/{project}', name: 'admin_activity_create_with_project', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_activity')")]
|
||||
#[IsGranted('create_activity')]
|
||||
public function createWithProjectAction(Request $request, Project $project): Response
|
||||
{
|
||||
return $this->createActivity($request, $project);
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_activity_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_activity')")]
|
||||
#[IsGranted('create_activity')]
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
return $this->createActivity($request, null);
|
||||
@@ -258,7 +259,7 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/permissions', name: 'admin_activity_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('permissions', activity)")]
|
||||
#[IsGranted('permissions', 'activity')]
|
||||
public function teamPermissionsAction(Activity $activity, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(ActivityTeamPermissionForm::class, $activity, [
|
||||
@@ -291,17 +292,16 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/create_team', name: 'activity_team_create', methods: ['GET'])]
|
||||
#[Security("is_granted('create_team') and is_granted('permissions', activity)")]
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('permissions', 'activity')]
|
||||
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository): Response
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', 'Team already existing');
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
if (null === $defaultTeam) {
|
||||
$defaultTeam = new Team($activity->getName());
|
||||
}
|
||||
|
||||
$defaultTeam = new Team($activity->getName());
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addActivity($activity);
|
||||
|
||||
@@ -315,7 +315,7 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'admin_activity_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
#[IsGranted('edit', 'activity')]
|
||||
public function editAction(Activity $activity, Request $request): Response
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
@@ -343,7 +343,7 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'admin_activity_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', activity)")]
|
||||
#[IsGranted('delete', 'activity')]
|
||||
public function deleteAction(Activity $activity, Request $request, ActivityStatisticService $statisticService): Response
|
||||
{
|
||||
$stats = $statisticService->getActivityStatistics($activity);
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace App\Controller\Auth;
|
||||
use App\Configuration\SamlConfigurationInterface;
|
||||
use App\Saml\SamlAuthFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
#[Route(path: '/saml')]
|
||||
final class SamlController extends AbstractController
|
||||
|
||||
@@ -15,16 +15,16 @@ use App\Entity\User;
|
||||
use App\Form\CalendarForm;
|
||||
use App\Timesheet\TrackingModeService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to display calendars.
|
||||
*/
|
||||
#[Route(path: '/calendar')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
final class CalendarController extends AbstractController
|
||||
{
|
||||
public function __construct(private CalendarService $calendarService, private SystemConfiguration $configuration, private TrackingModeService $service)
|
||||
|
||||
@@ -38,8 +38,8 @@ use App\Utils\DataTable;
|
||||
use App\Utils\FileHelper;
|
||||
use App\Utils\PageSetup;
|
||||
use JeroenDesloovere\VCard\VCard;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -48,12 +48,13 @@ use Symfony\Component\Intl\Countries;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to manage customer in the admin part of the site.
|
||||
*/
|
||||
#[Route(path: '/admin/customer')]
|
||||
#[Security("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')")]
|
||||
#[IsGranted(new Expression("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')"))]
|
||||
final class CustomerController extends AbstractController
|
||||
{
|
||||
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
@@ -62,7 +63,7 @@ final class CustomerController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_customer', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_customer_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request)
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -139,8 +140,8 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_customer_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_customer')")]
|
||||
public function createAction(Request $request, CustomerService $customerService)
|
||||
#[IsGranted('create_customer')]
|
||||
public function createAction(Request $request, CustomerService $customerService): Response
|
||||
{
|
||||
$customer = $customerService->createNewCustomer('');
|
||||
|
||||
@@ -148,8 +149,8 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('permissions', customer)")]
|
||||
public function teamPermissionsAction(Customer $customer, Request $request)
|
||||
#[IsGranted('permissions', 'customer')]
|
||||
public function teamPermissionsAction(Customer $customer, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
||||
'action' => $this->generateUrl('admin_customer_permissions', ['id' => $customer->getId()]),
|
||||
@@ -181,8 +182,8 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/comment_delete/{token}', name: 'customer_comment_delete', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")]
|
||||
public function deleteCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
|
||||
public function deleteCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
$customerId = $comment->getCustomer()->getId();
|
||||
|
||||
@@ -204,8 +205,8 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/comment_add', name: 'customer_comment_add', methods: ['POST'])]
|
||||
#[Security("is_granted('comments', customer)")]
|
||||
public function addCommentAction(Customer $customer, Request $request)
|
||||
#[IsGranted('comments', 'customer')]
|
||||
public function addCommentAction(Customer $customer, Request $request): Response
|
||||
{
|
||||
$comment = new CustomerComment($customer);
|
||||
$form = $this->getCommentForm($comment);
|
||||
@@ -224,8 +225,8 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/comment_pin/{token}', name: 'customer_comment_pin', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")]
|
||||
public function pinCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
|
||||
public function pinCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
$customerId = $comment->getCustomer()->getId();
|
||||
|
||||
@@ -248,17 +249,16 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/create_team', name: 'customer_team_create', methods: ['GET'])]
|
||||
#[Security("is_granted('create_team') and is_granted('permissions', customer)")]
|
||||
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository)
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('permissions', 'customer')]
|
||||
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository): Response
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', 'Team already existing');
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
if (null === $defaultTeam) {
|
||||
$defaultTeam = new Team($customer->getName());
|
||||
}
|
||||
|
||||
$defaultTeam = new Team($customer->getName());
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addCustomer($customer);
|
||||
|
||||
@@ -272,8 +272,8 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/projects/{page}', defaults: ['page' => 1], name: 'customer_projects', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', customer)")]
|
||||
public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository)
|
||||
#[IsGranted('view', 'customer')]
|
||||
public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -295,8 +295,8 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/details', name: 'customer_details', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', customer)")]
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository, CustomerStatisticService $statisticService)
|
||||
#[IsGranted('view', 'customer')]
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository, CustomerStatisticService $statisticService): Response
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -362,7 +362,7 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/vcard', name: 'customer_vcard', methods: ['GET'])]
|
||||
#[Security("is_granted('view', customer)")]
|
||||
#[IsGranted('view', 'customer')]
|
||||
public function downloadVCard(Customer $customer): Response
|
||||
{
|
||||
$vcard = new VCard();
|
||||
@@ -415,14 +415,14 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate/{rate}', name: 'admin_customer_rate_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
public function editRateAction(Customer $customer, CustomerRate $rate, Request $request, CustomerRateRepository $repository): Response
|
||||
{
|
||||
return $this->rateFormAction($customer, $rate, $request, $repository, $this->generateUrl('admin_customer_rate_edit', ['id' => $customer->getId(), 'rate' => $rate->getId()]));
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate', name: 'admin_customer_rate_add', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository): Response
|
||||
{
|
||||
$rate = new CustomerRate();
|
||||
@@ -459,15 +459,15 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
public function editAction(Customer $customer, Request $request)
|
||||
#[IsGranted('edit', 'customer')]
|
||||
public function editAction(Customer $customer, Request $request): Response
|
||||
{
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', customer)")]
|
||||
public function deleteAction(Customer $customer, Request $request, CustomerStatisticService $statisticService)
|
||||
#[IsGranted('delete', 'customer')]
|
||||
public function deleteAction(Customer $customer, Request $request, CustomerStatisticService $statisticService): Response
|
||||
{
|
||||
$stats = $statisticService->getCustomerStatistics($customer);
|
||||
|
||||
@@ -509,7 +509,7 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'customer_export', methods: ['GET'])]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
@@ -16,19 +16,19 @@ use App\Repository\BookmarkRepository;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Dashboard controller for the admin area.
|
||||
*/
|
||||
#[Route(path: '/dashboard')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
final class DashboardController extends AbstractController
|
||||
{
|
||||
public const BOOKMARK_TYPE = 'dashboard';
|
||||
|
||||
@@ -13,16 +13,16 @@ use App\Utils\FileHelper;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Utils\ReleaseVersion;
|
||||
use Composer\InstalledVersions;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
#[Route(path: '/doctor')]
|
||||
#[Security("is_granted('system_information')")]
|
||||
#[IsGranted('system_information')]
|
||||
final class DoctorController extends AbstractController
|
||||
{
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ final class DoctorController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/flush-log/{token}', name: 'doctor_flush_log', methods: ['GET'])]
|
||||
#[Security("is_granted('system_configuration')")]
|
||||
#[IsGranted('system_configuration')]
|
||||
public function deleteLogfileAction(string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('doctor.flush_log', $token))) {
|
||||
|
||||
@@ -16,17 +16,17 @@ use App\Export\TooManyItemsExportException;
|
||||
use App\Form\Toolbar\ExportToolbarForm;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to export timesheet data.
|
||||
*/
|
||||
#[Route(path: '/export')]
|
||||
#[Security("is_granted('create_export')")]
|
||||
#[IsGranted('create_export')]
|
||||
final class ExportController extends AbstractController
|
||||
{
|
||||
public function __construct(private ServiceExport $export)
|
||||
|
||||
@@ -11,23 +11,23 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\FavoriteRecordService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/favorite')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
final class FavoriteController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/timesheet/', name: 'favorites_timesheets', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
public function favoriteAction(): Response
|
||||
{
|
||||
return $this->render('partials/recent-activities.html.twig');
|
||||
}
|
||||
|
||||
#[Route(path: '/timesheet/add/{id}', name: 'favorites_timesheets_add', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
public function add(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
||||
{
|
||||
$favoriteRecordService->addFavorite($timesheet);
|
||||
@@ -36,7 +36,7 @@ final class FavoriteController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
public function remove(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
||||
{
|
||||
$favoriteRecordService->removeFavorite($timesheet);
|
||||
|
||||
@@ -11,16 +11,16 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Entity\User;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Homepage controller is a redirect controller with user specific logic.
|
||||
*/
|
||||
#[Route(path: '/homepage')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
final class HomepageController extends AbstractController
|
||||
{
|
||||
public const DEFAULT_ROUTE = 'timesheet';
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Invoice;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
@@ -37,7 +38,7 @@ use App\Repository\Query\InvoiceQuery;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
@@ -46,13 +47,16 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Controller used to create invoices and manage invoice templates.
|
||||
*/
|
||||
#[Route(path: '/invoice')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('view_invoice')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('view_invoice')]
|
||||
final class InvoiceController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
@@ -64,7 +68,7 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/', name: 'invoice', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_invoice')")]
|
||||
#[IsGranted('create_invoice')]
|
||||
public function indexAction(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
@@ -127,7 +131,8 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/preview/{customer}/{token}', name: 'invoice_preview', methods: ['GET'])]
|
||||
#[Security("is_granted('access', customer) and is_granted('create_invoice')")]
|
||||
#[IsGranted('create_invoice')]
|
||||
#[IsGranted('access', 'customer')]
|
||||
public function previewAction(Customer $customer, string $token, Request $request): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
@@ -167,7 +172,8 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/save-invoice/{customer}/{token}', name: 'invoice_create', methods: ['GET'])]
|
||||
#[Security("is_granted('access', customer) and is_granted('create_invoice')")]
|
||||
#[IsGranted('create_invoice')]
|
||||
#[IsGranted('access', 'customer')]
|
||||
public function createInvoiceAction(Customer $customer, string $token, Request $request, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
@@ -214,7 +220,8 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/change-status/{id}/{status}/{token}', name: 'admin_invoice_status', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('create_invoice')")]
|
||||
#[IsGranted('create_invoice')]
|
||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||
public function changeStatusAction(Invoice $invoice, string $status, string $token, Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
||||
@@ -250,7 +257,8 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/edit/{id}', name: 'admin_invoice_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('create_invoice')")]
|
||||
#[IsGranted('create_invoice')]
|
||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||
public function editAction(Invoice $invoice, Request $request): Response
|
||||
{
|
||||
$form = $this->createInvoiceEditForm($invoice);
|
||||
@@ -275,7 +283,8 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/delete/{id}/{token}', name: 'admin_invoice_delete', methods: ['GET'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('delete_invoice')")]
|
||||
#[IsGranted('delete_invoice')]
|
||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||
public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
||||
@@ -297,7 +306,8 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/download/{id}', name: 'admin_invoice_download', methods: ['GET'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('view_invoice')")]
|
||||
#[IsGranted('view_invoice')]
|
||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||
public function downloadAction(Invoice $invoice): Response
|
||||
{
|
||||
$file = $this->service->getInvoiceFile($invoice);
|
||||
@@ -312,7 +322,7 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/show/{page}', defaults: ['page' => 1], requirements: ['page' => '[1-9]\d*'], name: 'admin_invoice_list', methods: ['GET'])]
|
||||
#[Security("is_granted('view_invoice')")]
|
||||
#[IsGranted('view_invoice')]
|
||||
public function showInvoicesAction(Request $request, int $page): Response
|
||||
{
|
||||
$invoice = null;
|
||||
@@ -371,7 +381,7 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
|
||||
#[Security("is_granted('view_invoice')")]
|
||||
#[IsGranted('view_invoice')]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
{
|
||||
$query = new InvoiceArchiveQuery();
|
||||
@@ -394,7 +404,7 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/template/{page}', requirements: ['page' => '[1-9]\d*'], defaults: ['page' => 1], name: 'admin_invoice_template', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
#[IsGranted('manage_invoice_template')]
|
||||
public function listTemplateAction(int $page): Response
|
||||
{
|
||||
$query = new BaseQuery();
|
||||
@@ -431,15 +441,15 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/template/{id}/edit', name: 'admin_invoice_template_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
#[IsGranted('manage_invoice_template')]
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('upload_invoice_template')")]
|
||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository)
|
||||
#[IsGranted('upload_invoice_template')]
|
||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
$dir = $documentRepository->getUploadDirectory();
|
||||
$invoiceDir = $dir;
|
||||
@@ -448,6 +458,7 @@ final class InvoiceController extends AbstractController
|
||||
if ($invoiceDir[0] !== '/') {
|
||||
$invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir;
|
||||
}
|
||||
$invoiceDir = rtrim($invoiceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
|
||||
$used = [];
|
||||
foreach ($this->templateRepository->findAll() as $template) {
|
||||
@@ -503,23 +514,56 @@ final class InvoiceController extends AbstractController
|
||||
/** @var UploadedFile $uploadedFile */
|
||||
$uploadedFile = $form->get('document')->getData();
|
||||
|
||||
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
|
||||
$safeFilename = transliterator_transliterate(
|
||||
'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
|
||||
$originalFilename
|
||||
);
|
||||
$originalName = $uploadedFile->getClientOriginalName();
|
||||
$safeFilename = null;
|
||||
$extension = null;
|
||||
$success = true;
|
||||
|
||||
$extension = $uploadedFile->guessExtension();
|
||||
$allowed = InvoiceDocumentUploadForm::EXTENSIONS_NO_TWIG;
|
||||
if ((bool) $systemConfiguration->find('invoice.upload_twig') === true) {
|
||||
$allowed = InvoiceDocumentUploadForm::EXTENSIONS;
|
||||
}
|
||||
|
||||
$newFilename = substr($safeFilename, 0, 20) . '.' . $extension;
|
||||
foreach ($allowed as $ext) {
|
||||
$len = \strlen($ext);
|
||||
if (substr_compare($originalName, $ext, -$len) === 0) {
|
||||
$extension = $ext;
|
||||
$withoutExtension = str_replace($ext, '', $originalName);
|
||||
$safeFilename = transliterator_transliterate(InvoiceDocumentUploadForm::FILENAME_RULE, $withoutExtension);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$uploadedFile->move($invoiceDir, $newFilename);
|
||||
if ($safeFilename === null || $extension === null) {
|
||||
$success = false;
|
||||
$this->flashError('Invalid file given');
|
||||
} else {
|
||||
$newFilename = substr($safeFilename, 0, 20) . $extension;
|
||||
|
||||
try {
|
||||
$uploadedFile->move($invoiceDir, $newFilename);
|
||||
|
||||
// if this is a twig file, we directly try to compile the template
|
||||
if (stripos($newFilename, '.twig') !== false) {
|
||||
try {
|
||||
$twig->enableAutoReload();
|
||||
$twig->load('@invoice/' . $newFilename);
|
||||
$twig->disableAutoReload();
|
||||
} catch (Exception $ex) {
|
||||
unlink($invoiceDir . $newFilename);
|
||||
$success = false;
|
||||
$this->flashException($ex, 'File was deleted, as Twig template is broken: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$this->flashException($ex, 'action.upload.error');
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_document_upload');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashException($ex, 'action.upload.error');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,7 +582,7 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/document/{id}/delete/{token}', name: 'invoice_document_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
#[IsGranted('manage_invoice_template')]
|
||||
public function deleteDocument(string $id, string $token, CsrfTokenManagerInterface $csrfTokenManager, InvoiceDocumentRepository $documentRepository): Response
|
||||
{
|
||||
$document = $documentRepository->findByName($id);
|
||||
@@ -581,14 +625,14 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/template/create/{id}', name: 'admin_invoice_template_copy', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
#[IsGranted('manage_invoice_template')]
|
||||
public function copyTemplateAction(Request $request, InvoiceTemplate $copyFrom): Response
|
||||
{
|
||||
return $this->createTemplate($request, $copyFrom);
|
||||
}
|
||||
|
||||
#[Route(path: '/template/create', name: 'admin_invoice_template_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
#[IsGranted('manage_invoice_template')]
|
||||
public function createTemplateAction(Request $request): Response
|
||||
{
|
||||
return $this->createTemplate($request, null);
|
||||
@@ -608,7 +652,7 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/template/{id}/delete/{csrfToken}', name: 'admin_invoice_template_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
#[IsGranted('manage_invoice_template')]
|
||||
public function deleteTemplate(InvoiceTemplate $template, string $csrfToken, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_template', $csrfToken))) {
|
||||
|
||||
@@ -22,19 +22,20 @@ use App\Repository\UserRepository;
|
||||
use App\Security\RolePermissionManager;
|
||||
use App\Security\RoleService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to manage user roles and role permissions.
|
||||
*/
|
||||
#[Route(path: '/admin/permissions')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('role_permissions')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('role_permissions')]
|
||||
final class PermissionController extends AbstractController
|
||||
{
|
||||
public const TOKEN_NAME = 'user_role_permissions';
|
||||
@@ -44,7 +45,7 @@ final class PermissionController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService)
|
||||
{
|
||||
$all = $this->roleRepository->findAll();
|
||||
@@ -161,7 +162,7 @@ final class PermissionController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/roles/create', name: 'admin_user_roles', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function createRole(Request $request): Response
|
||||
{
|
||||
$role = new Role();
|
||||
@@ -195,7 +196,7 @@ final class PermissionController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/roles/{id}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function deleteRole(Role $role, string $csrfToken, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
|
||||
@@ -225,7 +226,7 @@ final class PermissionController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/roles/{id}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function savePermission(Role $role, string $name, bool $value, string $csrfToken, RolePermissionRepository $rolePermissionRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
|
||||
|
||||
@@ -11,15 +11,15 @@ namespace App\Controller;
|
||||
|
||||
use App\Plugin\PluginManager;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
#[Route(path: '/admin/plugins')]
|
||||
#[Security("is_granted('plugins')")]
|
||||
#[IsGranted('plugins')]
|
||||
final class PluginController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/', name: 'plugins', methods: ['GET'])]
|
||||
|
||||
@@ -32,18 +32,19 @@ use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* User profile controller
|
||||
*/
|
||||
#[Route(path: '/profile')]
|
||||
#[Security("(is_granted('view_own_profile') or is_granted('view_other_profile'))")]
|
||||
#[IsGranted(new Expression("is_granted('view_own_profile') or is_granted('view_other_profile')"))]
|
||||
final class ProfileController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/', name: 'my_profile', methods: ['GET'])]
|
||||
@@ -53,7 +54,7 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}', name: 'user_profile', methods: ['GET'])]
|
||||
#[Security("is_granted('view', profile)")]
|
||||
#[IsGranted('view', 'profile')]
|
||||
public function indexAction(User $profile, TimesheetRepository $repository, TimesheetStatisticService $statisticService): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
@@ -79,7 +80,7 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/edit', name: 'user_profile_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', profile)")]
|
||||
#[IsGranted('edit', 'profile')]
|
||||
public function editAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$form = $this->createEditForm($profile);
|
||||
@@ -101,7 +102,8 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/password', name: 'user_profile_password', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('password', profile)")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('password', 'profile')]
|
||||
public function passwordAction(User $profile, Request $request, UserService $userService): Response
|
||||
{
|
||||
$form = $this->createPasswordForm($profile);
|
||||
@@ -123,7 +125,8 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/api-token', name: 'user_profile_api_token', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('api-token', profile)")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('api-token', 'profile')]
|
||||
public function apiTokenAction(User $profile, Request $request, UserService $userService): Response
|
||||
{
|
||||
$form = $this->createApiTokenForm($profile);
|
||||
@@ -145,7 +148,8 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/roles', name: 'user_profile_roles', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('roles', profile)")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('roles', 'profile')]
|
||||
public function rolesAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$isSuperAdmin = $profile->isSuperAdmin();
|
||||
@@ -175,7 +179,7 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/teams', name: 'user_profile_teams', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('teams', profile)")]
|
||||
#[IsGranted('teams', 'profile')]
|
||||
public function teamsAction(User $profile, Request $request, UserRepository $userRepository, TeamRepository $teamRepository): Response
|
||||
{
|
||||
$originalMembers = new ArrayCollection();
|
||||
@@ -210,7 +214,7 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/prefs', name: 'user_profile_preferences', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('preferences', profile)")]
|
||||
#[IsGranted('preferences', 'profile')]
|
||||
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
@@ -336,7 +340,8 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/2fa', name: 'user_profile_2fa', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('2fa', profile)")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('2fa', 'profile')]
|
||||
public function twoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
if (!$profile->hasTotpSecret()) {
|
||||
@@ -382,7 +387,8 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/2fa_deactivate', name: 'user_profile_2fa_deactivate', methods: ['POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('2fa', profile)")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('2fa', 'profile')]
|
||||
public function deactivateTwoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
if ($profile->hasTotpSecret()) {
|
||||
@@ -401,7 +407,7 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/totp.png', name: 'user_profile_2fa_image', methods: ['GET'])]
|
||||
#[Security("is_granted('2fa', profile)")]
|
||||
#[IsGranted('2fa', 'profile')]
|
||||
public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
if (!$profile->hasTotpSecret()) {
|
||||
|
||||
@@ -40,20 +40,21 @@ use App\Repository\TeamRepository;
|
||||
use App\Utils\Context;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to manage projects.
|
||||
*/
|
||||
#[Route(path: '/admin/project')]
|
||||
#[Security("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')")]
|
||||
#[IsGranted(new Expression("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')"))]
|
||||
final class ProjectController extends AbstractController
|
||||
{
|
||||
public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService)
|
||||
@@ -62,7 +63,7 @@ final class ProjectController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_project', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_project_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request)
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -132,8 +133,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/permissions', name: 'admin_project_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('permissions', project)")]
|
||||
public function teamPermissions(Project $project, Request $request)
|
||||
#[IsGranted('permissions', 'project')]
|
||||
public function teamPermissions(Project $project, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
|
||||
'action' => $this->generateUrl('admin_project_permissions', ['id' => $project->getId()]),
|
||||
@@ -165,20 +166,20 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/create/{customer}', name: 'admin_project_create_with_customer', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_project')")]
|
||||
public function createWithCustomerAction(Request $request, Customer $customer)
|
||||
#[IsGranted('create_project')]
|
||||
public function createWithCustomerAction(Request $request, Customer $customer): Response
|
||||
{
|
||||
return $this->createProject($request, $customer);
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_project_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_project')")]
|
||||
public function createAction(Request $request)
|
||||
#[IsGranted('create_project')]
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
return $this->createProject($request, null);
|
||||
}
|
||||
|
||||
private function createProject(Request $request, ?Customer $customer = null)
|
||||
private function createProject(Request $request, ?Customer $customer = null): Response
|
||||
{
|
||||
$project = $this->projectService->createNewProject($customer);
|
||||
|
||||
@@ -204,8 +205,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/comment_delete/{token}', name: 'project_comment_delete', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")]
|
||||
public function deleteCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
|
||||
public function deleteCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
$projectId = $comment->getProject()->getId();
|
||||
|
||||
@@ -227,8 +228,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/comment_add', name: 'project_comment_add', methods: ['POST'])]
|
||||
#[Security("is_granted('comments', project)")]
|
||||
public function addCommentAction(Project $project, Request $request)
|
||||
#[IsGranted('comments', 'project')]
|
||||
public function addCommentAction(Project $project, Request $request): Response
|
||||
{
|
||||
$comment = new ProjectComment($project);
|
||||
$form = $this->getCommentForm($comment);
|
||||
@@ -247,8 +248,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/comment_pin/{token}', name: 'project_comment_pin', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")]
|
||||
public function pinCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
|
||||
public function pinCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
$projectId = $comment->getProject()->getId();
|
||||
|
||||
@@ -271,17 +272,16 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/create_team', name: 'project_team_create', methods: ['GET'])]
|
||||
#[Security("is_granted('create_team') and is_granted('permissions', project)")]
|
||||
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository)
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('permissions', 'project')]
|
||||
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository): Response
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', 'Team already existing');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
if (null === $defaultTeam) {
|
||||
$defaultTeam = new Team($project->getName());
|
||||
}
|
||||
|
||||
$defaultTeam = new Team($project->getName());
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addProject($project);
|
||||
|
||||
@@ -295,8 +295,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/activities/{page}', defaults: ['page' => 1], name: 'project_activities', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', project)")]
|
||||
public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository)
|
||||
#[IsGranted('view', 'project')]
|
||||
public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -319,8 +319,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/details', name: 'project_details', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', project)")]
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[IsGranted('view', 'project')]
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -380,14 +380,14 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate/{rate}', name: 'admin_project_rate_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
#[IsGranted('edit', 'project')]
|
||||
public function editRateAction(Project $project, ProjectRate $rate, Request $request, ProjectRateRepository $repository): Response
|
||||
{
|
||||
return $this->rateFormAction($project, $rate, $request, $repository, $this->generateUrl('admin_project_rate_edit', ['id' => $project->getId(), 'rate' => $rate->getId()]));
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate', name: 'admin_project_rate_add', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
#[IsGranted('edit', 'project')]
|
||||
public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository): Response
|
||||
{
|
||||
$rate = new ProjectRate();
|
||||
@@ -424,8 +424,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'admin_project_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
public function editAction(Project $project, Request $request)
|
||||
#[IsGranted('edit', 'project')]
|
||||
public function editAction(Project $project, Request $request): Response
|
||||
{
|
||||
$editForm = $this->createEditForm($project);
|
||||
$editForm->handleRequest($request);
|
||||
@@ -449,8 +449,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/duplicate/{token}', name: 'admin_project_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[IsGranted('edit', 'project')]
|
||||
public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) {
|
||||
$this->flashError('action.csrf.error');
|
||||
@@ -468,8 +468,8 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', project)")]
|
||||
public function deleteAction(Project $project, Request $request, ProjectStatisticService $statisticService)
|
||||
#[IsGranted('delete', 'project')]
|
||||
public function deleteAction(Project $project, Request $request, ProjectStatisticService $statisticService): Response
|
||||
{
|
||||
$stats = $statisticService->getProjectStatistics($project);
|
||||
|
||||
@@ -512,7 +512,7 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'project_export', methods: ['GET'])]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
@@ -17,15 +17,15 @@ use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to enter times in weekly form.
|
||||
*/
|
||||
#[Route(path: '/quick_entry')]
|
||||
#[Security("is_granted('quick-entry')")]
|
||||
#[IsGranted('quick-entry')]
|
||||
final class QuickEntryController extends AbstractController
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration, private TimesheetService $timesheetService, private TimesheetRepository $repository)
|
||||
|
||||
@@ -18,13 +18,14 @@ use App\Reporting\CustomerMonthlyProjects\CustomerMonthlyProjectsRepository;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/customer/monthly_projects')]
|
||||
#[Security("is_granted('report:customer') and is_granted('report:other')")]
|
||||
#[IsGranted('report:customer')]
|
||||
#[IsGranted('report:other')]
|
||||
final class CustomerMonthlyProjectsController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/view', name: 'report_customer_monthly_projects', methods: ['GET', 'POST'])]
|
||||
|
||||
@@ -14,14 +14,16 @@ use App\Form\Model\DateRange;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Reporting\ProjectDateRange\ProjectDateRangeForm;
|
||||
use App\Reporting\ProjectDateRange\ProjectDateRangeQuery;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectDateRangeController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/reporting/project_daterange', name: 'report_project_daterange', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
|
||||
@@ -14,14 +14,16 @@ use App\Project\ProjectStatisticService;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsForm;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectDetailsController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/reporting/project_details', name: 'report_project_details', methods: ['GET'])]
|
||||
#[Security("is_granted('report:project') and is_granted('details', 'project')")]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('details', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
|
||||
@@ -13,14 +13,16 @@ use App\Controller\AbstractController;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveForm;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectInactiveController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/reporting/project_inactive', name: 'report_project_inactive', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
|
||||
@@ -13,14 +13,16 @@ use App\Controller\AbstractController;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Reporting\ProjectView\ProjectViewForm;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectViewController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/reporting/project_view', name: 'report_project_view', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
|
||||
@@ -19,13 +19,13 @@ use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/users')]
|
||||
#[Security("is_granted('report:other')")]
|
||||
#[IsGranted('report:other')]
|
||||
final class ReportUsersMonthController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/month', name: 'report_monthly_users', methods: ['GET', 'POST'])]
|
||||
|
||||
@@ -19,13 +19,13 @@ use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/users')]
|
||||
#[Security("is_granted('report:other')")]
|
||||
#[IsGranted('report:other')]
|
||||
final class ReportUsersWeekController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/week', name: 'report_weekly_users', methods: ['GET', 'POST'])]
|
||||
|
||||
@@ -21,13 +21,13 @@ use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/users')]
|
||||
#[Security("is_granted('report:other')")]
|
||||
#[IsGranted('report:other')]
|
||||
final class ReportUsersYearController extends AbstractController
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -13,14 +13,14 @@ use App\Model\DailyStatistic;
|
||||
use App\Reporting\MonthByUser\MonthByUser;
|
||||
use App\Reporting\MonthByUser\MonthByUserForm;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/user')]
|
||||
#[Security("is_granted('report:user')")]
|
||||
#[IsGranted('report:user')]
|
||||
final class UserMonthController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -13,14 +13,14 @@ use App\Model\DailyStatistic;
|
||||
use App\Reporting\WeekByUser\WeekByUser;
|
||||
use App\Reporting\WeekByUser\WeekByUserForm;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/user')]
|
||||
#[Security("is_granted('report:user')")]
|
||||
#[IsGranted('report:user')]
|
||||
final class UserWeekController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -17,14 +17,14 @@ use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Reporting\YearByUser\YearByUserForm;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/user')]
|
||||
#[Security("is_granted('report:user')")]
|
||||
#[IsGranted('report:user')]
|
||||
final class UserYearController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -11,15 +11,15 @@ namespace App\Controller;
|
||||
|
||||
use App\Reporting\ReportingService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to render reports.
|
||||
*/
|
||||
#[Route(path: '/reporting')]
|
||||
#[Security("is_granted('view_reporting')")]
|
||||
#[IsGranted('view_reporting')]
|
||||
final class ReportingController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/', name: 'reporting', methods: ['GET'])]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\ConfigurationService;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Event\SystemConfigurationEvent;
|
||||
use App\Form\Model\Configuration;
|
||||
@@ -30,13 +31,11 @@ use App\Form\Type\TimezoneType;
|
||||
use App\Form\Type\TrackingModeType;
|
||||
use App\Form\Type\WeekDaysType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use App\Timesheet\LockdownService;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Validator\Constraints\ColorChoices;
|
||||
use App\Validator\Constraints\DateTimeFormat;
|
||||
use App\Validator\Constraints\TimeFormat;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
@@ -47,6 +46,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
@@ -57,10 +57,11 @@ use Symfony\Component\Validator\Constraints\Regex;
|
||||
* Controller used for executing system relevant tasks.
|
||||
*/
|
||||
#[Route(path: '/admin/system-config')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('system_configuration')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('system_configuration')]
|
||||
final class SystemConfigurationController extends AbstractController
|
||||
{
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private ConfigurationRepository $repository, private SystemConfiguration $systemConfiguration, private LockdownService $lockdownService)
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private ConfigurationService $repository, private SystemConfiguration $systemConfiguration, private LockdownService $lockdownService)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -141,7 +142,9 @@ final class SystemConfigurationController extends AbstractController
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->repository->saveSystemConfiguration($form->getData());
|
||||
/** @var SystemConfigurationModel $saveModel */
|
||||
$saveModel = $form->getData();
|
||||
$this->repository->saveSystemConfiguration($saveModel);
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->handleFormUpdateException($ex, $form);
|
||||
@@ -431,6 +434,7 @@ final class SystemConfigurationController extends AbstractController
|
||||
])
|
||||
->setRequired(true)
|
||||
->setType(TextType::class)
|
||||
->setConstraints([new NotBlank()])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
$authentication,
|
||||
@@ -458,6 +462,10 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setRequired(true)
|
||||
->setType(TextType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration('customer.rules.allow_duplicate_number'))
|
||||
->setLabel('customer.allow_duplicate_number')
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel('project'))
|
||||
->setConfiguration([
|
||||
|
||||
@@ -18,14 +18,14 @@ use App\Repository\Query\TagQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/admin/tags')]
|
||||
#[Security("is_granted('view_tag')")]
|
||||
#[IsGranted('view_tag')]
|
||||
final class TagController extends AbstractController
|
||||
{
|
||||
/**
|
||||
@@ -76,7 +76,7 @@ final class TagController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'tags_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_tag')")]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function editAction(Tag $tag, TagRepository $repository, Request $request)
|
||||
{
|
||||
$editForm = $this->createForm(TagEditForm::class, $tag, [
|
||||
@@ -108,7 +108,7 @@ final class TagController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'tags_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_tag')")]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function createAction(TagRepository $repository, Request $request)
|
||||
{
|
||||
$tag = new Tag();
|
||||
@@ -142,7 +142,7 @@ final class TagController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-delete', name: 'tags_multi_delete', methods: ['POST'])]
|
||||
#[Security("is_granted('delete_tag')")]
|
||||
#[IsGranted('delete_tag')]
|
||||
public function multiDelete(TagRepository $repository, Request $request)
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
|
||||
@@ -18,14 +18,14 @@ use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/admin/teams')]
|
||||
#[Security("is_granted('view_team')")]
|
||||
#[IsGranted('view_team')]
|
||||
final class TeamController extends AbstractController
|
||||
{
|
||||
public function __construct(private TeamRepository $repository)
|
||||
@@ -81,14 +81,15 @@ final class TeamController extends AbstractController
|
||||
* @return Response
|
||||
*/
|
||||
#[Route(path: '/create', name: 'admin_team_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_team')")]
|
||||
#[IsGranted('create_team')]
|
||||
public function createTeam(Request $request): Response
|
||||
{
|
||||
return $this->renderEditScreen(new Team(''), $request, true);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/duplicate', name: 'team_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', team) and is_granted('create_team')")]
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('edit', 'team')]
|
||||
public function duplicateTeam(Team $team, Request $request)
|
||||
{
|
||||
$newTeam = clone $team;
|
||||
@@ -103,14 +104,14 @@ final class TeamController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'admin_team_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', team)")]
|
||||
#[IsGranted('edit', 'team')]
|
||||
public function editAction(Team $team, Request $request)
|
||||
{
|
||||
return $this->renderEditScreen($team, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit_member', name: 'admin_team_member', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', team)")]
|
||||
#[IsGranted('edit', 'team')]
|
||||
public function editMemberAction(Team $team, Request $request)
|
||||
{
|
||||
$editForm = $this->createForm(TeamEditForm::class, $team, [
|
||||
|
||||
@@ -77,18 +77,18 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$table->setPaginationRoute($paginationRoute);
|
||||
$table->setReloadEvents('kimai.timesheetUpdate kimai.timesheetDelete');
|
||||
|
||||
$table->addColumn('date', ['class' => 'alwaysVisible', 'orderBy' => 'begin']);
|
||||
$table->addColumn('date', ['class' => 'alwaysVisible text-nowrap', 'orderBy' => 'begin']);
|
||||
|
||||
if ($this->canSeeStartEndTime()) {
|
||||
$table->addColumn('starttime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'begin']);
|
||||
$table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'end']);
|
||||
$table->addColumn('starttime', ['class' => 'd-none d-sm-table-cell text-center text-nowrap', 'orderBy' => 'begin']);
|
||||
$table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center text-nowrap', 'orderBy' => 'end']);
|
||||
}
|
||||
|
||||
$table->addColumn('duration', ['class' => 'text-end text-nowrap']);
|
||||
|
||||
if ($canSeeRate) {
|
||||
$table->addColumn('hourlyRate', ['class' => 'text-end d-none']);
|
||||
$table->addColumn('rate', ['class' => 'text-end']);
|
||||
$table->addColumn('hourlyRate', ['class' => 'text-end d-none text-nowrap']);
|
||||
$table->addColumn('rate', ['class' => 'text-end text-nowrap']);
|
||||
}
|
||||
|
||||
$table->addColumn('customer', ['class' => 'd-none d-md-table-cell']);
|
||||
@@ -98,7 +98,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$table->addColumn('tags', ['class' => 'd-none badges', 'orderBy' => false]);
|
||||
|
||||
foreach ($metaColumns as $metaColumn) {
|
||||
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false]);
|
||||
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'data' => $metaColumn]);
|
||||
}
|
||||
|
||||
if ($canSeeUsername) {
|
||||
@@ -116,11 +116,8 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'action_single' => $this->getActionNameSingle(),
|
||||
'canSeeUsername' => $canSeeUsername,
|
||||
'canSeeRate' => $canSeeRate,
|
||||
'stats' => $result->getStatistic(),
|
||||
'showSummary' => $this->includeSummary(),
|
||||
'showStartEndTime' => $this->canSeeStartEndTime(),
|
||||
'metaColumns' => $metaColumns,
|
||||
'allowMarkdown' => $this->hasMarkdownSupport(),
|
||||
'editRoute' => $this->getEditRoute()
|
||||
@@ -170,14 +167,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
protected function create(Request $request): Response
|
||||
{
|
||||
$entry = $this->service->createNewTimesheet($this->getUser());
|
||||
$entry = $this->service->createNewTimesheet($this->getUser(), $request);
|
||||
|
||||
$preForm = $this->createFormForGetRequest(TimesheetPreCreateForm::class, $entry, [
|
||||
'include_user' => $this->includeUserInForms('create'),
|
||||
]);
|
||||
$preForm->submit($request->query->all(), false);
|
||||
|
||||
$this->service->prepareNewTimesheet($entry, $request);
|
||||
$createForm = $this->getCreateForm($entry);
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
|
||||
@@ -13,19 +13,19 @@ use App\Entity\Timesheet;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Export\ServiceExport;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/timesheet')]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
final class TimesheetController extends TimesheetAbstractController
|
||||
{
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'timesheet', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'timesheet_paginated', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = $this->createDefaultQuery();
|
||||
@@ -35,42 +35,42 @@ final class TimesheetController extends TimesheetAbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export/', name: 'timesheet_export', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('export_own_timesheet')")]
|
||||
#[IsGranted('export_own_timesheet')]
|
||||
public function exportAction(Request $request, ServiceExport $serviceExport): Response
|
||||
{
|
||||
return $this->export($request, $serviceExport);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'timesheet_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', entry)")]
|
||||
#[IsGranted('edit', 'entry')]
|
||||
public function editAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->edit($entry, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/duplicate', name: 'timesheet_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('duplicate', entry)")]
|
||||
#[IsGranted('duplicate', 'entry')]
|
||||
public function duplicateAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->duplicate($entry, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-update', name: 'timesheet_multi_update', methods: ['POST'])]
|
||||
#[Security("is_granted('edit_own_timesheet')")]
|
||||
#[IsGranted('edit_own_timesheet')]
|
||||
public function multiUpdateAction(Request $request): Response
|
||||
{
|
||||
return $this->multiUpdate($request);
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-delete', name: 'timesheet_multi_delete', methods: ['POST'])]
|
||||
#[Security("is_granted('delete_own_timesheet')")]
|
||||
#[IsGranted('delete_own_timesheet')]
|
||||
public function multiDeleteAction(Request $request): Response
|
||||
{
|
||||
return $this->multiDelete($request);
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'timesheet_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_own_timesheet')")]
|
||||
#[IsGranted('create_own_timesheet')]
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
return $this->create($request);
|
||||
|
||||
@@ -21,19 +21,19 @@ use App\Form\TimesheetMultiUserEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/team/timesheet')]
|
||||
#[Security("is_granted('view_other_timesheet')")]
|
||||
#[IsGranted('view_other_timesheet')]
|
||||
final class TimesheetTeamController extends TimesheetAbstractController
|
||||
{
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_timesheet', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_timesheet_paginated', methods: ['GET'])]
|
||||
#[Security("is_granted('view_other_timesheet')")]
|
||||
#[IsGranted('view_other_timesheet')]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = $this->createDefaultQuery();
|
||||
@@ -43,35 +43,35 @@ final class TimesheetTeamController extends TimesheetAbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export/', name: 'admin_timesheet_export', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('export_other_timesheet')")]
|
||||
#[IsGranted('export_other_timesheet')]
|
||||
public function exportAction(Request $request, ServiceExport $serviceExport): Response
|
||||
{
|
||||
return $this->export($request, $serviceExport);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'admin_timesheet_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', entry)")]
|
||||
#[IsGranted('edit', 'entry')]
|
||||
public function editAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->edit($entry, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/duplicate', name: 'admin_timesheet_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('duplicate', entry)")]
|
||||
#[IsGranted('duplicate', 'entry')]
|
||||
public function duplicateAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->duplicate($entry, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_timesheet_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_other_timesheet')")]
|
||||
#[IsGranted('create_other_timesheet')]
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
return $this->create($request);
|
||||
}
|
||||
|
||||
#[Route(path: '/create_mu', name: 'admin_timesheet_create_multiuser', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_other_timesheet')")]
|
||||
#[IsGranted('create_other_timesheet')]
|
||||
public function createForMultiUserAction(Request $request): Response
|
||||
{
|
||||
$entry = new MultiUserTimesheet();
|
||||
@@ -150,14 +150,14 @@ final class TimesheetTeamController extends TimesheetAbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-update', name: 'admin_timesheet_multi_update', methods: ['POST'])]
|
||||
#[Security("is_granted('edit_other_timesheet')")]
|
||||
#[IsGranted('edit_other_timesheet')]
|
||||
public function multiUpdateAction(Request $request): Response
|
||||
{
|
||||
return $this->multiUpdate($request);
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-delete', name: 'admin_timesheet_multi_delete', methods: ['POST'])]
|
||||
#[Security("is_granted('delete_other_timesheet')")]
|
||||
#[IsGranted('delete_other_timesheet')]
|
||||
public function multiDeleteAction(Request $request): Response
|
||||
{
|
||||
return $this->multiDelete($request);
|
||||
|
||||
@@ -25,19 +25,20 @@ use App\Repository\UserRepository;
|
||||
use App\User\UserService;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to manage users in the admin part of the site.
|
||||
*/
|
||||
#[Route(path: '/admin/user')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('view_user')")]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('view_user')]
|
||||
final class UserController extends AbstractController
|
||||
{
|
||||
public function __construct(private UserPasswordHasherInterface $passwordHasher, private UserRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
@@ -46,7 +47,7 @@ final class UserController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_user', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_user_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request): Response
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -110,7 +111,7 @@ final class UserController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_user')")]
|
||||
#[IsGranted('create_user')]
|
||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository): Response
|
||||
{
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
@@ -139,7 +140,7 @@ final class UserController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'admin_user_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', userToDelete)")]
|
||||
#[IsGranted('delete', 'userToDelete')]
|
||||
public function deleteAction(User $userToDelete, Request $request, TimesheetRepository $repository, UserService $userService): Response
|
||||
{
|
||||
// $userToDelete MUST not be called $user, as $user is always the current user!
|
||||
@@ -191,7 +192,7 @@ final class UserController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'user_export', methods: ['GET'])]
|
||||
#[Security("is_granted('view_user')")]
|
||||
#[IsGranted('view_user')]
|
||||
public function exportAction(Request $request, UserExporter $exporter)
|
||||
{
|
||||
$query = new UserQuery();
|
||||
|
||||