Release 2.0.8 (#3914)

* support parsing negative durations in JS
* bump luxon dependency
* make sure that 2FA is not required for session based API calls
* show name of items to delete
* fix permission issue for recent activity items
This commit is contained in:
Kevin Papst
2023-03-13 01:48:53 +01:00
committed by GitHub
parent 1310285133
commit 8449eafcb6
20 changed files with 92 additions and 50 deletions

View File

@@ -295,6 +295,11 @@ export default class KimaiDateUtils extends KimaiPlugin {
return new Duration({seconds: 0}); return new Duration({seconds: 0});
} }
// actually, the parsing above should be improved, but that works as well
if (duration[0] === '-' && luxonDuration.valueOf() > 0) {
return luxonDuration.negate();
}
return luxonDuration; return luxonDuration;
} }

View File

@@ -43,7 +43,7 @@
"eslint-webpack-plugin": "^3.0", "eslint-webpack-plugin": "^3.0",
"gridstack": "^7.1", "gridstack": "^7.1",
"litepicker": "^2.0", "litepicker": "^2.0",
"luxon": "^3.0", "luxon": "^3.3",
"node-sass": "^8.0", "node-sass": "^8.0",
"sass-loader": "^13.2", "sass-loader": "^13.2",
"tom-select": "^2.0", "tom-select": "^2.0",

View File

@@ -6535,11 +6535,6 @@ parameters:
count: 1 count: 1
path: src/Repository/TimesheetRepository.php path: src/Repository/TimesheetRepository.php
-
message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/TimesheetRepository.php
- -
message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:findTimesheetsById\\(\\) should return array\\<App\\\\Entity\\\\Timesheet\\> but returns iterable\\<App\\\\Entity\\\\Timesheet\\>\\.$#" message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:findTimesheetsById\\(\\) should return array\\<App\\\\Entity\\\\Timesheet\\> but returns iterable\\<App\\\\Entity\\\\Timesheet\\>\\.$#"
count: 1 count: 1

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@
"app": { "app": {
"js": [ "js": [
"/build/runtime.f0079159.js", "/build/runtime.f0079159.js",
"/build/app.952a9f98.js" "/build/app.bd3fd9c1.js"
], ],
"css": [ "css": [
"/build/app.c429ff5b.css" "/build/app.c429ff5b.css"
@@ -63,7 +63,7 @@
}, },
"integrity": { "integrity": {
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV", "/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
"/build/app.952a9f98.js": "sha384-qK3Hcem6As5Jy3ZiVz2rlABsJUIvNMQ0Dq6RfRBalbq1l88/zFfE5ZRXVMYtLP73", "/build/app.bd3fd9c1.js": "sha384-lz7W4QdKZkFcM0r1sScURXhxrv/ezU2DXgjGgubQsorHuaQDcKKoucOvSK3yxW/U",
"/build/app.c429ff5b.css": "sha384-m7RJ9g2fW3tENrf+oi/FsH8wTu6fsjuhvpDztfqr+YjNN0AvCapER6yvmGkqwp+I", "/build/app.c429ff5b.css": "sha384-m7RJ9g2fW3tENrf+oi/FsH8wTu6fsjuhvpDztfqr+YjNN0AvCapER6yvmGkqwp+I",
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW", "/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg", "/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",

View File

@@ -1,6 +1,6 @@
{ {
"build/app.css": "/build/app.c429ff5b.css", "build/app.css": "/build/app.c429ff5b.css",
"build/app.js": "/build/app.952a9f98.js", "build/app.js": "/build/app.bd3fd9c1.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.587575e7.js", "build/export-pdf.js": "/build/export-pdf.587575e7.js",
"build/invoice.css": "/build/invoice.3c80ee80.css", "build/invoice.css": "/build/invoice.3c80ee80.css",

View File

@@ -9,6 +9,7 @@
namespace App\API\Authentication; namespace App\API\Authentication;
use Scheb\TwoFactorBundle\Security\Http\Authenticator\TwoFactorAuthenticator;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -24,6 +25,19 @@ final class SessionAuthenticator extends AbstractAuthenticator
{ {
} }
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
$token = parent::createToken($passport, $firewallName);
// this should not be necessary, as /api/ is excluded from 2FA process, but just to make sure this
// authenticator never triggers 2FA, we add the attribute to the token
// https://symfony.com/bundles/SchebTwoFactorBundle/6.x/custom_conditions.html
$token->setAttribute(TwoFactorAuthenticator::FLAG_2FA_COMPLETE, true);
return $token;
}
public function supports(Request $request): ?bool public function supports(Request $request): ?bool
{ {
if (str_contains($request->getRequestUri(), '/api/')) { if (str_contains($request->getRequestUri(), '/api/')) {

View File

@@ -17,11 +17,11 @@ class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.0.7'; public const VERSION = '2.0.8';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 20007; public const VERSION_ID = 20008;
/** /**
* The software name * The software name
*/ */

View File

@@ -358,7 +358,9 @@ class TimesheetRepository extends EntityRepository
* *
* Should a teamlead: * Should a teamlead:
* 1. see all records of his team-members, even if they recorded times for projects invisible to him * 1. see all records of his team-members, even if they recorded times for projects invisible to him
* 2. only see records for projects which can be accessed by hom (current situation) * 2. only see records for projects which can be accessed by him (current situation)
*
* @param array<Team> $teams
*/ */
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): bool private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): bool
{ {
@@ -637,6 +639,7 @@ class TimesheetRepository extends EntityRepository
public function getRecentActivities(User $user, DateTime $startFrom = null, int $limit = 10): array public function getRecentActivities(User $user, DateTime $startFrom = null, int $limit = 10): array
{ {
return $this->findTimesheetsById( return $this->findTimesheetsById(
$user,
$this->getRecentActivityIds($user, $startFrom, $limit) $this->getRecentActivityIds($user, $startFrom, $limit)
); );
} }
@@ -653,13 +656,11 @@ class TimesheetRepository extends EntityRepository
// do NOT join the customer and do NOT check the customer visibility, as this // do NOT join the customer and do NOT check the customer visibility, as this
// will dramatically increase the speed of this (otherwise slow) query // will dramatically increase the speed of this (otherwise slow) query
// ->join('p.customer', 'c')
// ->andWhere($qb->expr()->eq('c.visible', ':visible')) // ->andWhere($qb->expr()->eq('c.visible', ':visible'))
// you might want to join activity and project to check their visibility // you might want to join activity and project to check their visibility
// but for now this is way slower than simply fetching more items // but for now this is way slower than simply fetching more items
// //
// ->join('t.project', 'p')
// ->andWhere($qb->expr()->eq('p.visible', ':visible')) // ->andWhere($qb->expr()->eq('p.visible', ':visible'))
// ->join('t.activity', 'a') // ->join('t.activity', 'a')
// ->andWhere($qb->expr()->eq('a.visible', ':visible')) // ->andWhere($qb->expr()->eq('a.visible', ':visible'))
@@ -680,6 +681,11 @@ class TimesheetRepository extends EntityRepository
->setParameter('begin', $startFrom); ->setParameter('begin', $startFrom);
} }
$qb->join('t.project', 'p');
$qb->join('p.customer', 'c');
$this->addPermissionCriteria($qb, $user);
$results = $qb->getQuery()->getScalarResult(); $results = $qb->getQuery()->getScalarResult();
if (empty($results)) { if (empty($results)) {
@@ -690,10 +696,13 @@ class TimesheetRepository extends EntityRepository
} }
/** /**
* @param User $user
* @param array<int> $ids * @param array<int> $ids
* @param bool $fullyHydrated
* @param bool $basicHydrated
* @return array<Timesheet> * @return array<Timesheet>
*/ */
public function findTimesheetsById(array $ids, bool $fullyHydrated = false, bool $basicHydrated = true): array public function findTimesheetsById(User $user, array $ids, bool $fullyHydrated = false, bool $basicHydrated = true): array
{ {
if (\count($ids) === 0) { if (\count($ids) === 0) {
return []; return [];
@@ -706,6 +715,11 @@ class TimesheetRepository extends EntityRepository
->orderBy('t.end', 'DESC') ->orderBy('t.end', 'DESC')
; ;
$qb->join('t.project', 'p');
$qb->join('p.customer', 'c');
$this->addPermissionCriteria($qb, $user);
return $this->getHydratedResultsByQuery($qb, $fullyHydrated, $basicHydrated); return $this->getHydratedResultsByQuery($qb, $fullyHydrated, $basicHydrated);
} }

View File

@@ -39,7 +39,7 @@ final class FavoriteRecordService
$recentIds = $this->repository->getRecentActivityIds($user, null, $limit); $recentIds = $this->repository->getRecentActivityIds($user, null, $limit);
} }
/** @var array<int> $ids */ /** @var array<int> $ids */
$ids = \array_slice(array_unique(array_merge($favIds, $recentIds)), 0, $limit); $ids = array_unique(array_merge($favIds, $recentIds));
/** @var array<int, bool|FavoriteTimesheet> $favorites */ /** @var array<int, bool|FavoriteTimesheet> $favorites */
$favorites = []; $favorites = [];
@@ -49,7 +49,7 @@ final class FavoriteRecordService
$all = []; $all = [];
if (\count($ids) > 0) { if (\count($ids) > 0) {
$timesheets = $this->repository->findTimesheetsById($ids, false, false); $timesheets = $this->repository->findTimesheetsById($user, $ids, false, false);
foreach ($timesheets as $timesheet) { foreach ($timesheets as $timesheet) {
$id = $timesheet->getId(); $id = $timesheet->getId();
if ($id === null) { if ($id === null) {
@@ -69,7 +69,7 @@ final class FavoriteRecordService
} }
} }
return array_values($all); return \array_slice(array_values($all), 0, $limit);
} }
private function getBookmark(User $user): Bookmark private function getBookmark(User $user): Bookmark

View File

@@ -8,6 +8,7 @@
{{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', { {{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans), 'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form, 'form': form,
'delete_item_name': activity.name,
'used': (stats.counter > 0), 'used': (stats.counter > 0),
'back': path('admin_activity') 'back': path('admin_activity')
}) }} }) }}

View File

@@ -8,6 +8,7 @@
{{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', { {{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans), 'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form, 'form': form,
'delete_item_name': customer.name,
'used': (stats.counter > 0), 'used': (stats.counter > 0),
'back': path('admin_customer') 'back': path('admin_customer')
}) }} }) }}

View File

@@ -7,6 +7,9 @@
{% endblock %} {% endblock %}
{% block box_body %} {% block box_body %}
{% block form_body %} {% block form_body %}
{% if delete_item_name is defined %}
<p><strong>{{ delete_item_name }}</strong></p>
{% endif %}
{% if used is same as (false) %} {% if used is same as (false) %}
{{ 'delete.not_in_use'|trans }} {{ 'delete.not_in_use'|trans }}
<div class="d-none"> <div class="d-none">

View File

@@ -6,6 +6,9 @@
{{ title|default('confirm.delete'|trans) }} {{ title|default('confirm.delete'|trans) }}
{% endblock %} {% endblock %}
{% block modal_body %} {% block modal_body %}
{% if delete_item_name is defined %}
<p><strong>{{ delete_item_name }}</strong></p>
{% endif %}
{% if used is same as (false) %} {% if used is same as (false) %}
{{ 'delete.not_in_use'|trans }} {{ 'delete.not_in_use'|trans }}
<div class="d-none"> <div class="d-none">

View File

@@ -1,4 +1,4 @@
{% set favorites = favorite_timesheets(app.user) %} {% set favorites = favorite_timesheets(app.user, 10) %}
{% if favorites|length > 0 %} {% if favorites|length > 0 %}
{% from 'macros/widgets.html.twig' import label_customer, label_project, label_activity %} {% from 'macros/widgets.html.twig' import label_customer, label_project, label_activity %}
{% set class = "btn-outline-white" %} {% set class = "btn-outline-white" %}
@@ -6,34 +6,38 @@
{% set class = "btn-dark" %} {% set class = "btn-dark" %}
{% endif %} {% endif %}
<div class="list-group list-group-flush list-group-hoverable menu"> <div class="list-group list-group-flush list-group-hoverable menu">
{% set counter = 0 %}
{% for favorite in favorites %} {% for favorite in favorites %}
{% set entry = favorite.getTimesheet() %} {% set entry = favorite.getTimesheet() %}
<div class="list-group-item"> {% if is_granted('start', entry) and counter < 7 %}
<div class="row align-items-center"> {% set counter = counter + 1 %}
<div class="col text-truncate" > <div class="list-group-item">
<a class="api-link text-decoration-none text-body d-block" href="{{ path('restart_timesheet', {'id': entry.id}) }}" <div class="row align-items-center">
data-event="kimai.timesheetStart kimai.timesheetUpdate kimai.closeRemoteModal" data-method="PATCH" data-msg-error="timesheet.start.error" <div class="col text-truncate" >
data-msg-success="timesheet.start.success"> <a class="api-link text-decoration-none text-body d-block" href="{{ path('restart_timesheet', {'id': entry.id}) }}"
{{ label_activity(entry.activity) }} data-event="kimai.timesheetStart kimai.timesheetUpdate kimai.closeRemoteModal" data-method="PATCH" data-msg-error="timesheet.start.error"
<div class="d-block text-truncate mt-n1"> data-msg-success="timesheet.start.success">
{{ label_project(entry.project) }} {{ label_activity(entry.activity) }}
{{ label_customer(entry.project.customer) }} <div class="d-block text-truncate mt-n1">
</div> {{ label_project(entry.project) }}
</a> {{ label_customer(entry.project.customer) }}
</div> </div>
<div class="col-auto"> </a>
{% if favorite.isFavorite() %} </div>
<a href="{{ path('favorites_timesheets_remove', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions show remote-modal-reload"> <div class="col-auto">
<i class="text-yellow {{ 'bookmarked'|icon }}"></i> {% if favorite.isFavorite() %}
</a> <a href="{{ path('favorites_timesheets_remove', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions show remote-modal-reload">
{% else %} <i class="text-yellow {{ 'bookmarked'|icon }}"></i>
<a href="{{ path('favorites_timesheets_add', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions remote-modal-reload"> </a>
<i class="{{ 'bookmark'|icon }}"></i> {% else %}
</a> <a href="{{ path('favorites_timesheets_add', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions remote-modal-reload">
{% endif %} <i class="{{ 'bookmark'|icon }}"></i>
</a>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -8,6 +8,7 @@
{{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', { {{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans), 'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form, 'form': form,
'delete_item_name': project.name,
'used': (stats.counter > 0), 'used': (stats.counter > 0),
'back': path('admin_project') 'back': path('admin_project')
}) }} }) }}

View File

@@ -13,6 +13,7 @@
{{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', { {{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans), 'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form, 'form': form,
'delete_item_name': user.displayName,
'used': inUse, 'used': inUse,
'back': path('admin_user') 'back': path('admin_user')
}) }} }) }}

View File

@@ -5159,7 +5159,7 @@ __metadata:
eslint-webpack-plugin: ^3.0 eslint-webpack-plugin: ^3.0
gridstack: ^7.1 gridstack: ^7.1
litepicker: ^2.0 litepicker: ^2.0
luxon: ^3.0 luxon: ^3.3
node-sass: ^8.0 node-sass: ^8.0
sass-loader: ^13.2 sass-loader: ^13.2
tom-select: ^2.0 tom-select: ^2.0
@@ -5311,10 +5311,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"luxon@npm:^3.0": "luxon@npm:^3.3":
version: 3.1.0 version: 3.3.0
resolution: "luxon@npm:3.1.0" resolution: "luxon@npm:3.3.0"
checksum: f8a850b759ba7a2e009d904c522ed7bc264bf4add57578f8948e52a0ed96b627b025b5aad8032295b570ae19fac41f0ffab91bdb128715fb0cc020798a7ba886 checksum: 50cf17a0dc155c3dcacbeae8c0b7e80db425e0ba97b9cbdf12a7fc142d841ff1ab1560919f033af46240ed44e2f70c49f76e3422524c7fc8bb8d81ca47c66187
languageName: node languageName: node
linkType: hard linkType: hard