Compare commits
88 Commits
2.12.0
...
api-pagina
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233c0e792e | ||
|
|
896e8b42c1 | ||
|
|
bf288bf115 | ||
|
|
13649e146c | ||
|
|
0f9b964b11 | ||
|
|
4076e1c3d3 | ||
|
|
8de54e1fa7 | ||
|
|
1965c35b43 | ||
|
|
3fd770047e | ||
|
|
036b297a76 | ||
|
|
537c120ad9 | ||
|
|
3e0dadc0c8 | ||
|
|
004ac5a6b9 | ||
|
|
3204dcb03e | ||
|
|
b1903ba183 | ||
|
|
75fa7a20b8 | ||
|
|
a19426147a | ||
|
|
9d933f62c0 | ||
|
|
9e3d243b4b | ||
|
|
62047c7c01 | ||
|
|
0bcd45babe | ||
|
|
2a72815481 | ||
|
|
ff6a3e8262 | ||
|
|
0947b88d36 | ||
|
|
5de5b4fef5 | ||
|
|
cde856a9c1 | ||
|
|
6f8877a740 | ||
|
|
e7d71bff64 | ||
|
|
cca0657291 | ||
|
|
a4904d3335 | ||
|
|
b9c1036b21 | ||
|
|
818c080d28 | ||
|
|
a5a6ae065b | ||
|
|
8b06ca9d10 | ||
|
|
7e1025d61d | ||
|
|
8788311faf | ||
|
|
ce22520d7f | ||
|
|
f5d76cfd9a | ||
|
|
27f7f14539 | ||
|
|
f7c32e633a | ||
|
|
6d9055826f | ||
|
|
4627a9047e | ||
|
|
987b46bf8f | ||
|
|
8792a1df09 | ||
|
|
a8678892d6 | ||
|
|
72350b76c0 | ||
|
|
8578bbe033 | ||
|
|
93ca983e0b | ||
|
|
e29ef25581 | ||
|
|
a617eba535 | ||
|
|
0c445d1bc4 | ||
|
|
5f7114e008 | ||
|
|
e2c511cf46 | ||
|
|
e042ef6bfa | ||
|
|
c059e51b3c | ||
|
|
661bb16342 | ||
|
|
94d6b43dcc | ||
|
|
99c296a751 | ||
|
|
8f8d228fb3 | ||
|
|
1214e508fe | ||
|
|
7d394fbe56 | ||
|
|
fd4dfc6be4 | ||
|
|
9dc9c71a46 | ||
|
|
345bb6601e | ||
|
|
5c8c21d609 | ||
|
|
7219b3f421 | ||
|
|
b76a5d5c35 | ||
|
|
40a9027d34 | ||
|
|
553bd20e06 | ||
|
|
3ea5803e08 | ||
|
|
4949afb098 | ||
|
|
d328badca1 | ||
|
|
175d371ef9 | ||
|
|
6bb7b38252 | ||
|
|
afe0656502 | ||
|
|
dd51c8dfba | ||
|
|
b6c98f871d | ||
|
|
19b2d47591 | ||
|
|
a636683dee | ||
|
|
c1f5d3def4 | ||
|
|
3e88a007d0 | ||
|
|
1fdd11e94c | ||
|
|
2ac783a300 | ||
|
|
3a358b1434 | ||
|
|
216da19cc9 | ||
|
|
dee90bb15e | ||
|
|
a78e8ed8c4 | ||
|
|
3e3cdc5e69 |
@@ -2,7 +2,6 @@
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /opt/kimai/public
|
||||
|
||||
PassEnv DATABASE_PREFIX
|
||||
PassEnv MAILER_FROM
|
||||
PassEnv APP_ENV
|
||||
PassEnv APP_SECRET
|
||||
|
||||
208
.docker/Dockerfile.kimai-base
Normal file
208
.docker/Dockerfile.kimai-base
Normal file
@@ -0,0 +1,208 @@
|
||||
# Source base [fpm/apache]
|
||||
ARG BASE="fpm"
|
||||
ARG PHP_VER="8.2"
|
||||
ARG COMPOSER_VER="latest"
|
||||
ARG TIMEZONE="Europe/London"
|
||||
|
||||
###########################
|
||||
# Shared tools
|
||||
###########################
|
||||
|
||||
# composer base image
|
||||
FROM composer:${COMPOSER_VER} AS composer
|
||||
|
||||
###########################
|
||||
# PHP extensions
|
||||
###########################
|
||||
|
||||
#fpm alpine php extension base
|
||||
FROM php:${PHP_VER}-fpm-alpine AS fpm-php-ext-base
|
||||
RUN apk add --no-cache \
|
||||
# build-tools
|
||||
autoconf \
|
||||
dpkg \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
icu-dev \
|
||||
libatomic \
|
||||
libc-dev \
|
||||
libgomp \
|
||||
libmagic \
|
||||
m4 \
|
||||
make \
|
||||
mpc1 \
|
||||
mpfr4 \
|
||||
musl-dev \
|
||||
perl \
|
||||
re2c \
|
||||
# gd
|
||||
freetype-dev \
|
||||
libpng-dev \
|
||||
# icu
|
||||
icu-dev \
|
||||
icu-data-full \
|
||||
# ldap
|
||||
openldap-dev \
|
||||
libldap \
|
||||
# zip
|
||||
libzip-dev \
|
||||
# xsl
|
||||
libxslt-dev
|
||||
|
||||
|
||||
# apache debian php extension base
|
||||
FROM php:${PHP_VER}-apache-bookworm AS apache-php-ext-base
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y \
|
||||
libldap2-dev \
|
||||
libicu-dev \
|
||||
libpng-dev \
|
||||
libzip-dev \
|
||||
libxslt1-dev \
|
||||
libfreetype6-dev
|
||||
|
||||
|
||||
# php extension gd - 13.86s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-gd
|
||||
RUN docker-php-ext-configure gd \
|
||||
--with-freetype && \
|
||||
docker-php-ext-install -j$(nproc) gd
|
||||
|
||||
# php extension intl : 15.26s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-intl
|
||||
RUN docker-php-ext-install -j$(nproc) intl
|
||||
|
||||
# php extension ldap : 8.45s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-ldap
|
||||
RUN docker-php-ext-configure ldap && \
|
||||
docker-php-ext-install -j$(nproc) ldap
|
||||
|
||||
# php extension pdo_mysql : 6.14s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-pdo_mysql
|
||||
RUN docker-php-ext-install -j$(nproc) pdo_mysql
|
||||
|
||||
# php extension zip : 8.18s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-zip
|
||||
RUN docker-php-ext-install -j$(nproc) zip
|
||||
|
||||
# php extension xsl : ?.?? s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-xsl
|
||||
RUN docker-php-ext-install -j$(nproc) xsl
|
||||
|
||||
# php extension redis
|
||||
FROM ${BASE}-php-ext-base AS php-ext-redis
|
||||
RUN yes no | pecl install redis && \
|
||||
docker-php-ext-enable redis
|
||||
|
||||
# php extension opcache
|
||||
FROM ${BASE}-php-ext-base AS php-ext-opcache
|
||||
RUN docker-php-ext-install -j$(nproc) opcache
|
||||
|
||||
###########################
|
||||
# fpm base build
|
||||
###########################
|
||||
|
||||
# fpm base build
|
||||
FROM php:${PHP_VER}-fpm-alpine AS fpm-base
|
||||
ARG TIMEZONE
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
coreutils \
|
||||
freetype \
|
||||
haveged \
|
||||
icu \
|
||||
icu-data-full \
|
||||
libldap \
|
||||
libpng \
|
||||
libzip \
|
||||
libxslt-dev \
|
||||
fcgi \
|
||||
tzdata && \
|
||||
touch /use_fpm
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD \
|
||||
SCRIPT_NAME=/ping \
|
||||
SCRIPT_FILENAME=/ping \
|
||||
REQUEST_METHOD=GET \
|
||||
cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# apache base build
|
||||
###########################
|
||||
|
||||
FROM php:${PHP_VER}-apache-bookworm AS apache-base
|
||||
ARG TIMEZONE
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
bash \
|
||||
haveged \
|
||||
libicu72 \
|
||||
libldap-common \
|
||||
libpng16-16 \
|
||||
libzip4 \
|
||||
libxslt1.1 \
|
||||
libfreetype6 && \
|
||||
echo "Listen 8001" > /etc/apache2/ports.conf && \
|
||||
a2enmod rewrite && \
|
||||
touch /use_apache
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD curl -f http://127.0.0.1:8001 || exit 1
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# global base build
|
||||
###########################
|
||||
|
||||
FROM ${BASE}-base AS base
|
||||
ARG TIMEZONE
|
||||
|
||||
ENV TIMEZONE=${TIMEZONE}
|
||||
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
|
||||
# make composer home dir
|
||||
mkdir /composer && \
|
||||
chown -R www-data:www-data /composer && \
|
||||
echo "echo -e This imnage is not intended to be used stand alone. It's a base image\necho for the Kimai project. https://www.kimai.org\n" > /entrypoint.sh && \
|
||||
chmod 777 /entrypoint.sh
|
||||
# copy composer
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# copy php extensions
|
||||
|
||||
# PHP extension xsl
|
||||
COPY --from=php-ext-xsl /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini
|
||||
COPY --from=php-ext-xsl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so
|
||||
# PHP extension pdo_mysql
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so
|
||||
# PHP extension zip
|
||||
COPY --from=php-ext-zip /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
|
||||
COPY --from=php-ext-zip /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so
|
||||
# PHP extension ldap
|
||||
COPY --from=php-ext-ldap /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini
|
||||
COPY --from=php-ext-ldap /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so
|
||||
# PHP extension gd
|
||||
COPY --from=php-ext-gd /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
|
||||
COPY --from=php-ext-gd /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so
|
||||
# PHP extension intl
|
||||
COPY --from=php-ext-intl /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
|
||||
COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so
|
||||
# PHP extension redis
|
||||
COPY --from=php-ext-redis /usr/local/etc/php/conf.d/docker-php-ext-redis.ini /usr/local/etc/php/conf.d/docker-php-ext-redis.ini
|
||||
COPY --from=php-ext-redis /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so
|
||||
COPY --from=php-ext-opcache /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
|
||||
|
||||
CMD [ "/entrypoint.sh" ]
|
||||
|
||||
# docker build -t kimai/kimai-base:fpm --build-arg BASE=fpm .
|
||||
# docker build -t kimai/kimai-base:apache --build-arg BASE=apache .
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
$DB_HOST = $argv[1];
|
||||
$DB_BASE = $argv[2];
|
||||
$DB_HOST = urldecode($argv[1]);
|
||||
$DB_BASE = urldecode($argv[2]);
|
||||
$DB_PORT = $argv[3];
|
||||
$DB_USER = $argv[4];
|
||||
$DB_PASS = $argv[5];
|
||||
$DB_USER = urldecode($argv[4]);
|
||||
$DB_PASS = urldecode($argv[5]);
|
||||
|
||||
echo "Testing DB:";
|
||||
echo "*";
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=5.7.40"
|
||||
fi
|
||||
|
||||
/opt/kimai/bin/console kimai:version
|
||||
if [ $? != 0 ]; then
|
||||
echo "PHP/Kimai not responding"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test FPM CGI
|
||||
if [ -f /use_fpm ]; then
|
||||
echo Testing FPM
|
||||
php-fpm -t
|
||||
fi
|
||||
|
||||
# Test Apache/httpd
|
||||
if [ -f /use_apache ]; then
|
||||
echo Testing Apache
|
||||
apache2ctl -t
|
||||
fi
|
||||
|
||||
exit 0
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: [kevinpapst]
|
||||
custom: ["https://www.kimai.org/donate/", "https://www.paypal.me/kevinpapst"]
|
||||
custom: ["https://www.kimai.org/", "https://www.kimai.cloud/", "https://www.kevinpapst.de/"]
|
||||
|
||||
2
.github/release-drafter.yml
vendored
2
.github/release-drafter.yml
vendored
@@ -21,8 +21,6 @@ version-resolver:
|
||||
- 'translation'
|
||||
default: patch
|
||||
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/)
|
||||
|
||||
**Compatible with PHP 8.1 to 8.3**
|
||||
|
||||
$CHANGES
|
||||
|
||||
43
.github/workflows/docker-base.yaml
vendored
Normal file
43
.github/workflows/docker-base.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: 'Docker base image'
|
||||
# act -j build -W ./.github/workflows/docker-base.yaml --secret-file .dockerhub.secrets
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 4 5 * *" # build on the 5th of every month
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
server: [ fpm, apache ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .docker
|
||||
file: .docker/Dockerfile.kimai-base
|
||||
build-args: |
|
||||
TIMEZONE=Europe/London
|
||||
BASE=${{ matrix.server }}
|
||||
platforms: linux/amd64,linux/arm64 #,linux/arm/v8,linux/arm/v7,linux/arm/v6
|
||||
tags: |
|
||||
kimai/kimai-base:${{ matrix.server }}
|
||||
push: true
|
||||
|
||||
|
||||
75
.github/workflows/docker.yaml
vendored
75
.github/workflows/docker.yaml
vendored
@@ -10,7 +10,6 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
type: [ dev, prod ]
|
||||
server: [ fpm, apache ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -20,13 +19,13 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Install lastversion
|
||||
run: sudo apt-get update -y; sudo apt-get install --no-install-recommends -y python3 python3-pip; pip install lastversion
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||
@@ -36,21 +35,34 @@ jobs:
|
||||
run: echo "kimai_version=$(lastversion https://github.com/kimai/kimai)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
KIMAI=${{ env.kimai_version }}
|
||||
TIMEZONE=Europe/London
|
||||
BASE=${{ matrix.server }}
|
||||
target: ${{ matrix.type }}
|
||||
platforms: linux/amd64,linux/arm64 #,linux/arm/v8,linux/arm/v7,linux/arm/v6
|
||||
target: prod
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
kimai/kimai2:${{ matrix.server }}-${{ matrix.type }}
|
||||
kimai/kimai2:${{ matrix.server }}-${{ env.kimai_version }}-${{ matrix.type }}
|
||||
kimai/kimai2:${{ matrix.server }}
|
||||
kimai/kimai2:${{ matrix.server }}-${{ env.kimai_version }}
|
||||
push: true
|
||||
|
||||
- name: Build dev image
|
||||
if: matrix.server == 'apache'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
KIMAI=${{ env.kimai_version }}
|
||||
TIMEZONE=Europe/London
|
||||
BASE=apache
|
||||
target: dev
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
kimai/kimai2:dev
|
||||
push: true
|
||||
- name: Test Lite
|
||||
run: docker run --rm --entrypoint /assets/test-lite.sh kimai/kimai2:${{ matrix.server }}-${{ matrix.type }}
|
||||
|
||||
tag:
|
||||
needs: build
|
||||
@@ -58,49 +70,18 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||
|
||||
- name: Install buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Pull images
|
||||
run: |
|
||||
docker pull kimai/kimai2:fpm-prod
|
||||
docker pull kimai/kimai2:fpm-dev
|
||||
docker pull kimai/kimai2:apache-prod
|
||||
docker pull kimai/kimai2:apache-dev
|
||||
|
||||
- name: Tag fpm
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:fpm
|
||||
docker push kimai/kimai2:fpm
|
||||
|
||||
- name: Tag fpm latest
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:fpm-latest
|
||||
docker push kimai/kimai2:fpm-latest
|
||||
docker pull kimai/kimai2:fpm
|
||||
|
||||
- name: Tag latest
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:latest
|
||||
docker push kimai/kimai2:latest
|
||||
|
||||
- name: Tag prod
|
||||
run: |
|
||||
docker tag kimai/kimai2:fpm-prod kimai/kimai2:prod
|
||||
docker push kimai/kimai2:prod
|
||||
|
||||
- name: Tag apache
|
||||
run: |
|
||||
docker tag kimai/kimai2:apache-prod kimai/kimai2:apache
|
||||
docker push kimai/kimai2:apache
|
||||
|
||||
- name: Tag apache latest
|
||||
run: |
|
||||
docker tag kimai/kimai2:apache-prod kimai/kimai2:apache-latest
|
||||
docker push kimai/kimai2:apache-latest
|
||||
|
||||
- name: Tag dev
|
||||
run: |
|
||||
docker tag kimai/kimai2:apache-dev kimai/kimai2:dev
|
||||
docker push kimai/kimai2:dev
|
||||
docker buildx imagetools create -t kimai/kimai2:latest kimai/kimai2:fpm
|
||||
|
||||
29
.github/workflows/lock-threads.yaml
vendored
Normal file
29
.github/workflows/lock-threads.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-inactive-days: '90'
|
||||
issue-comment: >
|
||||
This thread has been automatically locked since there has not been any recent activity after it was closed.
|
||||
Please share your experience with the community and [leave a testimonial](https://love.kimai.org/) to support Kimai.
|
||||
issue-lock-reason: 'resolved'
|
||||
pr-inactive-days: '180'
|
||||
log-output: true
|
||||
56
.github/workflows/lock.yaml
vendored
56
.github/workflows/lock.yaml
vendored
@@ -1,56 +0,0 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
exclude-issue-created-before: ''
|
||||
exclude-issue-created-after: ''
|
||||
exclude-issue-created-between: ''
|
||||
exclude-issue-closed-before: ''
|
||||
exclude-issue-closed-after: ''
|
||||
exclude-issue-closed-between: ''
|
||||
include-any-issue-labels: ''
|
||||
include-all-issue-labels: ''
|
||||
exclude-any-issue-labels: ''
|
||||
add-issue-labels: ''
|
||||
remove-issue-labels: ''
|
||||
issue-comment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
If you use Kimai on a daily basis, please [consider donating](https://www.kimai.org/donate/) to
|
||||
support further development of Kimai.
|
||||
issue-lock-reason: 'resolved'
|
||||
pr-inactive-days: '180'
|
||||
exclude-pr-created-before: ''
|
||||
exclude-pr-created-after: ''
|
||||
exclude-pr-created-between: ''
|
||||
exclude-pr-closed-before: ''
|
||||
exclude-pr-closed-after: ''
|
||||
exclude-pr-closed-between: ''
|
||||
include-any-pr-labels: ''
|
||||
include-all-pr-labels: ''
|
||||
exclude-any-pr-labels: ''
|
||||
add-pr-labels: ''
|
||||
remove-pr-labels: ''
|
||||
pr-comment: ''
|
||||
pr-lock-reason: ''
|
||||
process-only: ''
|
||||
log-output: false
|
||||
2
.github/workflows/release-drafter.yaml
vendored
2
.github/workflows/release-drafter.yaml
vendored
@@ -25,6 +25,6 @@ jobs:
|
||||
needs: correct_repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
6
.github/workflows/testing.yaml
vendored
6
.github/workflows/testing.yaml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
run: composer install
|
||||
|
||||
- name: Validate Composer
|
||||
run: composer validate --strict
|
||||
run: composer validate --strict --no-check-all
|
||||
|
||||
- name: Warmup cache
|
||||
run: APP_ENV=dev bin/console kimai:reload -n
|
||||
@@ -93,7 +93,6 @@ jobs:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||
APP_ENV: test
|
||||
MAILER_URL: null://localhost
|
||||
TEST_WITH_BUNDLES: 1
|
||||
|
||||
- name: Full test-suite with coverage
|
||||
if: matrix.php == '8.2'
|
||||
@@ -102,11 +101,10 @@ jobs:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||
APP_ENV: dev
|
||||
MAILER_URL: null://localhost
|
||||
TEST_WITH_BUNDLES: 1
|
||||
|
||||
- name: Upload code coverage
|
||||
if: matrix.php == '8.2'
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.xml
|
||||
|
||||
2
.github/workflows/website.yaml
vendored
2
.github/workflows/website.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Emit repository_dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.WEBSITE_ACCESS_TOKEN }}
|
||||
repository: kimai/www.kimai.org
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -59,4 +59,6 @@
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
###< symfony/webpack-encore-bundle ###
|
||||
/nbproject/*
|
||||
/nbproject/*
|
||||
|
||||
.dockerhub.secrets
|
||||
|
||||
@@ -9,6 +9,7 @@ COMMENT;
|
||||
|
||||
$fixer = new PhpCsFixer\Config();
|
||||
$fixer
|
||||
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'encoding' => true,
|
||||
|
||||
200
Dockerfile
200
Dockerfile
@@ -28,164 +28,16 @@ RUN apk add --no-cache git && \
|
||||
|
||||
# production kimai source
|
||||
FROM git-dev AS git-prod
|
||||
|
||||
WORKDIR /opt/kimai
|
||||
RUN rm -r tests
|
||||
|
||||
# composer base image
|
||||
FROM composer:${COMPOSER_VER} AS composer
|
||||
|
||||
###########################
|
||||
# PHP extensions
|
||||
###########################
|
||||
# FPM base
|
||||
FROM kimai/kimai-base:fpm AS fpm-base
|
||||
|
||||
#fpm alpine php extension base
|
||||
FROM php:${PHP_VER}-fpm-alpine AS fpm-php-ext-base
|
||||
RUN apk add --no-cache \
|
||||
# build-tools
|
||||
autoconf \
|
||||
dpkg \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
icu-dev \
|
||||
libatomic \
|
||||
libc-dev \
|
||||
libgomp \
|
||||
libmagic \
|
||||
m4 \
|
||||
make \
|
||||
mpc1 \
|
||||
mpfr4 \
|
||||
musl-dev \
|
||||
perl \
|
||||
re2c \
|
||||
# gd
|
||||
freetype-dev \
|
||||
libpng-dev \
|
||||
# icu
|
||||
icu-dev \
|
||||
icu-data-full \
|
||||
# ldap
|
||||
openldap-dev \
|
||||
libldap \
|
||||
# zip
|
||||
libzip-dev \
|
||||
# xsl
|
||||
libxslt-dev
|
||||
|
||||
|
||||
# apache debian php extension base
|
||||
FROM php:${PHP_VER}-apache-bookworm AS apache-php-ext-base
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y \
|
||||
libldap2-dev \
|
||||
libicu-dev \
|
||||
libpng-dev \
|
||||
libzip-dev \
|
||||
libxslt1-dev \
|
||||
libfreetype6-dev
|
||||
|
||||
|
||||
# php extension gd - 13.86s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-gd
|
||||
RUN docker-php-ext-configure gd \
|
||||
--with-freetype && \
|
||||
docker-php-ext-install -j$(nproc) gd
|
||||
|
||||
# php extension intl : 15.26s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-intl
|
||||
RUN docker-php-ext-install -j$(nproc) intl
|
||||
|
||||
# php extension ldap : 8.45s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-ldap
|
||||
RUN docker-php-ext-configure ldap && \
|
||||
docker-php-ext-install -j$(nproc) ldap
|
||||
|
||||
# php extension pdo_mysql : 6.14s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-pdo_mysql
|
||||
RUN docker-php-ext-install -j$(nproc) pdo_mysql
|
||||
|
||||
# php extension zip : 8.18s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-zip
|
||||
RUN docker-php-ext-install -j$(nproc) zip
|
||||
|
||||
# php extension xsl : ?.?? s
|
||||
FROM ${BASE}-php-ext-base AS php-ext-xsl
|
||||
RUN docker-php-ext-install -j$(nproc) xsl
|
||||
|
||||
# php extension redis
|
||||
FROM ${BASE}-php-ext-base AS php-ext-redis
|
||||
RUN yes no | pecl install redis && \
|
||||
docker-php-ext-enable redis
|
||||
|
||||
# php extension opcache
|
||||
FROM ${BASE}-php-ext-base AS php-ext-opcache
|
||||
RUN docker-php-ext-install -j$(nproc) opcache
|
||||
|
||||
###########################
|
||||
# fpm base build
|
||||
###########################
|
||||
|
||||
# fpm base build
|
||||
FROM php:${PHP_VER}-fpm-alpine AS fpm-base
|
||||
ARG KIMAI
|
||||
ARG TIMEZONE
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
coreutils \
|
||||
freetype \
|
||||
haveged \
|
||||
icu \
|
||||
icu-data-full \
|
||||
libldap \
|
||||
libpng \
|
||||
libzip \
|
||||
libxslt-dev \
|
||||
fcgi \
|
||||
tzdata && \
|
||||
touch /use_fpm
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD \
|
||||
SCRIPT_NAME=/ping \
|
||||
SCRIPT_FILENAME=/ping \
|
||||
REQUEST_METHOD=GET \
|
||||
cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# apache base build
|
||||
###########################
|
||||
|
||||
FROM php:${PHP_VER}-apache-bookworm AS apache-base
|
||||
ARG KIMAI
|
||||
ARG TIMEZONE
|
||||
# Apache base
|
||||
FROM kimai/kimai-base:apache AS apache-base
|
||||
COPY .docker/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
bash \
|
||||
haveged \
|
||||
libicu72 \
|
||||
libldap-common \
|
||||
libpng16-16 \
|
||||
libzip4 \
|
||||
libxslt1.1 \
|
||||
libfreetype6 && \
|
||||
echo "Listen 8001" > /etc/apache2/ports.conf && \
|
||||
a2enmod rewrite && \
|
||||
touch /use_apache
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||
CMD curl -f http://127.0.0.1:8001 || exit 1
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# global base build
|
||||
@@ -195,14 +47,21 @@ FROM ${BASE}-base AS base
|
||||
ARG KIMAI
|
||||
ARG TIMEZONE
|
||||
|
||||
LABEL maintainer="tobias@neontribe.co.uk"
|
||||
LABEL maintainer="bastian@schroll-software.de"
|
||||
LABEL org.opencontainers.image.title="Kimai" \
|
||||
org.opencontainers.image.description="Kimai is a time-tracking application." \
|
||||
org.opencontainers.image.authors="Kimai Community" \
|
||||
org.opencontainers.image.url="https://www.kimai.org/" \
|
||||
org.opencontainers.image.documentation="https://www.kimai.org/documentation/" \
|
||||
org.opencontainers.image.source="https://github.com/kimai/kimai" \
|
||||
org.opencontainers.image.version="${KIMAI}" \
|
||||
org.opencontainers.image.vendor="Kevin Papst" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.base.name="docker.io/library/alpine"
|
||||
|
||||
ENV KIMAI=${KIMAI}
|
||||
ENV TIMEZONE=${TIMEZONE}
|
||||
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
|
||||
# make composer home dir
|
||||
mkdir /composer && \
|
||||
mkdir -p /composer && \
|
||||
chown -R www-data:www-data /composer
|
||||
|
||||
# copy startup script & DB checking script
|
||||
@@ -211,34 +70,6 @@ COPY .docker/service.sh /service.sh
|
||||
COPY .docker/self-test.sh /self-test.sh
|
||||
COPY .docker/dbtest.php /dbtest.php
|
||||
|
||||
# copy composer
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# copy php extensions
|
||||
|
||||
# PHP extension xsl
|
||||
COPY --from=php-ext-xsl /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini
|
||||
COPY --from=php-ext-xsl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so
|
||||
# PHP extension pdo_mysql
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini
|
||||
COPY --from=php-ext-pdo_mysql /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so
|
||||
# PHP extension zip
|
||||
COPY --from=php-ext-zip /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
|
||||
COPY --from=php-ext-zip /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so
|
||||
# PHP extension ldap
|
||||
COPY --from=php-ext-ldap /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini
|
||||
COPY --from=php-ext-ldap /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so
|
||||
# PHP extension gd
|
||||
COPY --from=php-ext-gd /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
|
||||
COPY --from=php-ext-gd /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so
|
||||
# PHP extension intl
|
||||
COPY --from=php-ext-intl /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
|
||||
COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so
|
||||
# PHP extension redis
|
||||
COPY --from=php-ext-redis /usr/local/etc/php/conf.d/docker-php-ext-redis.ini /usr/local/etc/php/conf.d/docker-php-ext-redis.ini
|
||||
COPY --from=php-ext-redis /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so
|
||||
COPY --from=php-ext-opcache /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
|
||||
|
||||
ENV DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=5.7.40"
|
||||
ENV APP_SECRET=change_this_to_something_unique
|
||||
# The default container name for nginx is nginx
|
||||
@@ -266,7 +97,6 @@ VOLUME [ "/opt/kimai/var" ]
|
||||
CMD [ "/startup.sh" ]
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# final builds
|
||||
###########################
|
||||
|
||||
28
README.md
28
README.md
@@ -26,9 +26,7 @@ and so much more.
|
||||
There are two [versions](https://www.kimai.org/documentation/versions.html) of Kimai existing:
|
||||
|
||||
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1+)
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — EOL since mid of 2023 (PHP 7.4)
|
||||
|
||||
Do **NOT** use Version 1, it won't get any more updates!
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — do **NOT** use, EOL since mid of 2023 (PHP 7.4)
|
||||
|
||||
### Links
|
||||
|
||||
@@ -38,23 +36,20 @@ Do **NOT** use Version 1, it won't get any more updates!
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.1.3 minimum
|
||||
- PHP 8.1.3 minimum (support for PHP 8.2 and 8.3)
|
||||
- MariaDB or MySQL
|
||||
- A webserver and subdomain (subdirectory is not supported)
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
||||
|
||||
Support for PHP 8.2 and 8.3.
|
||||
|
||||
## Installation
|
||||
|
||||
- [Recommended setup](https://www.kimai.org/documentation/installation.html#recommended-setup) — with Git and Composer
|
||||
- [Docker](https://hub.docker.com/r/kimai/kimai2) — containerized by [@tobybatch](https://github.com/tobybatch/kimai2)
|
||||
- Caddy with Docker-Compose at [Hetzner](https://www.kimai.org/documentation/hosting-hetzner-cloud.html) and [DigitalOcean](https://www.kimai.org/documentation/hosting-digital-ocean.html)
|
||||
- [SSH setup](https://www.kimai.org/documentation/installation.html) with Git and Composer
|
||||
- [Docker images](https://hub.docker.com/r/kimai/kimai2) with FPM only or incl. Apache
|
||||
- [Synology](https://www.kimai.org/documentation/synology.html) user can host the Docker version
|
||||
- [Developer setups](https://www.kimai.org/documentation/developers.html) if you want to create Kimai integrations
|
||||
|
||||
There are also documentations for:
|
||||
- [developer setups](https://www.kimai.org/documentation/developers.html) — on your local machine
|
||||
- [shared hostings](https://www.kimai.org/documentation/installation.html#shared-hosting) — the least favorable option
|
||||
- [Synology](https://www.kimai.org/documentation/synology.html) — you could try to host the Docker version instead
|
||||
- [1-click installer](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations) — hosted environments
|
||||
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
|
||||
|
||||
And if you don't want to host Kimai, you can use [the Cloud version](https://www.kimai.cloud/) of it.
|
||||
|
||||
@@ -65,13 +60,12 @@ And if you don't want to host Kimai, you can use [the Cloud version](https://www
|
||||
|
||||
### Plugins
|
||||
|
||||
- [Plugin marketplace](https://www.kimai.org/store/) — find existing plugins here
|
||||
- [Plugins](https://www.kimai.org/store/) — paid and free plugin marketplace
|
||||
- [Developer documentation](https://www.kimai.org/documentation/developers.html) — how to create a plugin
|
||||
|
||||
## Roadmap and releases
|
||||
|
||||
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.
|
||||
You can see a rough development [roadmap](https://github.com/orgs/kimai/projects/2), which is open for changes and input from the community, your [ideas](https://github.com/kimai/kimai/issues) are welcome.
|
||||
|
||||
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.
|
||||
@@ -83,7 +77,7 @@ The best way to start is to [open a new issue](https://github.com/kimai/kimai/is
|
||||
|
||||
In case you want to contribute, but you wouldn't know how, here are some suggestions:
|
||||
|
||||
- Spread the word: 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!
|
||||
- Spread the word: Please [write a testimonial for our Wall of love](https://love.kimai.org), vote for Kimai on any software platform, you can toot or tweet about it, share it on LinkedIn, Reddit and any other social media platform!
|
||||
- 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.
|
||||
|
||||
20
SECURITY.md
20
SECURITY.md
@@ -1,27 +1,11 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As announced in the [README](README.md) I only support the latest available release and `main` branch.
|
||||
As announced in the [README](README.md) security fixes will only be added to the `main` branch.
|
||||
|
||||
| Version | Supported |
|
||||
|----------------------|--------------------|
|
||||
| main branch | :white_check_mark: |
|
||||
| latest minor release | :white_check_mark: |
|
||||
| older releases | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please read the [Bughunter](https://www.kimai.org/documentation/bughunter.html) documentation before posting.
|
||||
You can report any security related vulnerability in the [advisory section at GitHub](https://github.com/kimai/kimai/security/advisories)
|
||||
or via email to [support@kimai.org](mailto:support@kimai.org).
|
||||
|
||||
I will work as fast as I can to fix the problem and publish a bugfix release / security update.
|
||||
Depending on the size of the required fixes, this might take a couple of hours or a couple of days.
|
||||
|
||||
You can expect that your message will be answered ASAP.
|
||||
|
||||
You will be mentioned in the release notes if your issue is valid.
|
||||
I am grateful for any (discrete) disclosure of vulnerabilities!
|
||||
|
||||
Please note: if you are asking for money, I will not reply. I receive these scam messages every day...
|
||||
You find all information in our [Bughunter documentation](https://www.kimai.org/documentation/bughunter.html).
|
||||
|
||||
14
UPGRADING.md
14
UPGRADING.md
@@ -8,9 +8,21 @@ 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.14.0](https://github.com/kimai/kimai/releases/tag/2.13.0)
|
||||
|
||||
**DEVELOPERS ONLY**
|
||||
|
||||
The API now uses pagination globally for all collection endpoints. This might break current implementations
|
||||
|
||||
All collections are limited to 50 entries by default (changes in Customer, Project, Activity, User).
|
||||
|
||||
The parameters to influence pagination, page size and page number are `size` (and `pageSize`) and `page`.
|
||||
|
||||
Read the [API pagination docs](https://www.kimai.org/documentation/api-pagination.html) for more infos.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings ([.env](https://github.com/kimai/kimai/issues/4246) or in your [Docker](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
Colors,
|
||||
// SubTitle
|
||||
} from 'chart.js';
|
||||
|
||||
@@ -50,6 +51,7 @@ Chart.register(
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
Colors
|
||||
// SubTitle
|
||||
);
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import KimaiNotification from "./plugins/KimaiNotification";
|
||||
import KimaiHotkeys from "./plugins/KimaiHotkeys";
|
||||
import KimaiRemoteModal from "./plugins/KimaiRemoteModal";
|
||||
import KimaiUser from "./plugins/KimaiUser";
|
||||
import KimaiAutocompleteTags from "./forms/KimaiAutocompleteTags";
|
||||
|
||||
export default class KimaiLoader {
|
||||
|
||||
@@ -70,6 +71,7 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiDateRangePicker('input[data-daterangepicker="on"]'));
|
||||
kimai.registerPlugin(new KimaiDatePicker('input[data-datepicker="on"]'));
|
||||
kimai.registerPlugin(new KimaiAutocomplete());
|
||||
kimai.registerPlugin(new KimaiAutocompleteTags());
|
||||
kimai.registerPlugin(new KimaiTimesheetForm());
|
||||
kimai.registerPlugin(new KimaiTeamForm());
|
||||
kimai.registerPlugin(new KimaiCopyDataForm());
|
||||
@@ -83,7 +85,7 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
|
||||
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
|
||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form', ['td.multiCheckbox', 'td.actions']));
|
||||
kimai.registerPlugin(new KimaiRemoteModal());
|
||||
kimai.registerPlugin(new KimaiActiveRecords());
|
||||
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
*/
|
||||
|
||||
import TomSelect from 'tom-select';
|
||||
import KimaiFormPlugin from "./KimaiFormPlugin";
|
||||
import KimaiFormTomselectPlugin from "./KimaiFormTomselectPlugin";
|
||||
|
||||
/**
|
||||
* Supporting auto-complete fields via API.
|
||||
* Used for timesheet tagging in toolbar and edit dialogs.
|
||||
*/
|
||||
export default class KimaiAutocomplete extends KimaiFormPlugin {
|
||||
export default class KimaiAutocomplete extends KimaiFormTomselectPlugin {
|
||||
|
||||
init()
|
||||
{
|
||||
@@ -28,11 +27,23 @@ export default class KimaiAutocomplete extends KimaiFormPlugin {
|
||||
return true;
|
||||
}
|
||||
|
||||
activateForm(form)
|
||||
{
|
||||
loadData(apiUrl, query, callback) {
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = this.getContainer().getPlugin('api');
|
||||
|
||||
API.get(apiUrl, {'name': query}, (data) => {
|
||||
let results = [];
|
||||
for (let item of data) {
|
||||
results.push({text: item.name, value: item.name});
|
||||
}
|
||||
callback(results);
|
||||
}, () => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
activateForm(form)
|
||||
{
|
||||
[].slice.call(form.querySelectorAll(this.selector)).map((node) => {
|
||||
const apiUrl = node.dataset['autocompleteUrl'];
|
||||
let minChars = 3;
|
||||
@@ -40,7 +51,7 @@ export default class KimaiAutocomplete extends KimaiFormPlugin {
|
||||
minChars = parseInt(node.dataset['minimumCharacter']);
|
||||
}
|
||||
|
||||
new TomSelect(node, {
|
||||
let options = {
|
||||
// see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257
|
||||
onItemAdd: function(){
|
||||
// remove remaining characters from input after selecting an item
|
||||
@@ -58,36 +69,21 @@ export default class KimaiAutocomplete extends KimaiFormPlugin {
|
||||
return query.length >= minChars;
|
||||
},
|
||||
load: (query, callback) => {
|
||||
API.get(apiUrl, {'name': query}, (data) => {
|
||||
const results = [].slice.call(data).map((result) => {
|
||||
return {text: result, value: result};
|
||||
});
|
||||
callback(results);
|
||||
}, () => {
|
||||
callback();
|
||||
});
|
||||
this.loadData(apiUrl, query, callback);
|
||||
},
|
||||
render: {
|
||||
// eslint-disable-next-line
|
||||
not_loading: (data, escape) => {
|
||||
// no default content
|
||||
},
|
||||
option_create: (data, escape) => {
|
||||
const name = escape(data.input);
|
||||
if (name.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const tpl = this.translate('select.search.create');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>')
|
||||
return '<div class="create">' + tplReplaced + '</div>';
|
||||
},
|
||||
no_results: (data, escape) => {
|
||||
const tpl = this.translate('select.search.notfound');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>')
|
||||
return '<div class="no-results">' + tplReplaced + '</div>';
|
||||
},
|
||||
};
|
||||
|
||||
let render = {
|
||||
// eslint-disable-next-line
|
||||
not_loading: (data, escape) => {
|
||||
// no default content
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const rendererType = (node.dataset['renderer'] !== undefined) ? node.dataset['renderer'] : 'default';
|
||||
options.render = {...render, ...this.getRenderer(rendererType)};
|
||||
|
||||
new TomSelect(node, options);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
34
assets/js/forms/KimaiAutocompleteTags.js
Normal file
34
assets/js/forms/KimaiAutocompleteTags.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import KimaiAutocomplete from "./KimaiAutocomplete";
|
||||
|
||||
/**
|
||||
* Used for timesheet tagging in toolbar and edit dialogs.
|
||||
*/
|
||||
export default class KimaiAutocompleteTags extends KimaiAutocomplete {
|
||||
|
||||
init()
|
||||
{
|
||||
this.selector = '[data-form-widget="tags"]';
|
||||
}
|
||||
|
||||
loadData(apiUrl, query, callback) {
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = this.getContainer().getPlugin('api');
|
||||
|
||||
API.get(apiUrl, {'name': query}, (data) => {
|
||||
let results = [];
|
||||
for (let item of data) {
|
||||
results.push({text: item.name, value: item.name, color: item['color-safe']});
|
||||
}
|
||||
callback(results);
|
||||
}, () => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
*/
|
||||
|
||||
import TomSelect from 'tom-select';
|
||||
import KimaiFormPlugin from "./KimaiFormPlugin";
|
||||
import KimaiFormTomselectPlugin from "./KimaiFormTomselectPlugin";
|
||||
|
||||
export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
||||
|
||||
constructor(selector, apiSelects)
|
||||
{
|
||||
@@ -82,30 +82,20 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
plugins: plugins,
|
||||
// if there are more than X entries, the other ones are hidden and can only be found
|
||||
// by typing some characters to trigger the internal option search
|
||||
// see App\Form\Type\TagsType::MAX_AMOUNT_SELECT
|
||||
maxOptions: 500,
|
||||
sortField:[{field: '$order'}, {field: '$score'}],
|
||||
};
|
||||
|
||||
let render = {
|
||||
option_create: (data, escape) => {
|
||||
const name = escape(data.input);
|
||||
if (name.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const tpl = this.translate('select.search.create');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>');
|
||||
return '<div class="create">' + tplReplaced + '</div>';
|
||||
},
|
||||
no_results: (data, escape) => {
|
||||
const tpl = this.translate('select.search.notfound');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>');
|
||||
return '<div class="no-results">' + tplReplaced + '</div>';
|
||||
},
|
||||
onOptionAdd: (value) => {
|
||||
node.dispatchEvent(new CustomEvent('create', {detail: {'value': value}}));
|
||||
},
|
||||
};
|
||||
|
||||
const rendererType = (node.dataset['renderer'] !== undefined) ? node.dataset['renderer'] : 'default';
|
||||
options.render = {...render, ...this.getRenderer(rendererType)};
|
||||
|
||||
if (node.dataset['create'] !== undefined) {
|
||||
options = {...options, ...{
|
||||
persist: true,
|
||||
@@ -124,44 +114,6 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
}};
|
||||
}
|
||||
|
||||
if (node.dataset['renderer'] !== undefined && node.dataset['renderer'] === 'color') {
|
||||
options.render = {...render, ...{
|
||||
option: function(data, escape) {
|
||||
let item = '<div class="list-group-item border-0 p-1 ps-2 text-nowrap">';
|
||||
if (data.color !== undefined) {
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||
} else {
|
||||
item += '<span class="color-choice-item"> </span>';
|
||||
}
|
||||
item += escape(data.text) + '</div>';
|
||||
return item;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
let item = '<div class="text-nowrap">';
|
||||
if (data.color !== undefined) {
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||
} else {
|
||||
item += '<span class="color-choice-item"> </span>';
|
||||
}
|
||||
item += escape(data.text) + '</div>';
|
||||
return item;
|
||||
}
|
||||
}};
|
||||
} else {
|
||||
options.render = {...render, ...{
|
||||
// the empty entry would collapse and only show as a tiny 5px line if there is no content inside
|
||||
option: function(data, escape) {
|
||||
let text = data.text;
|
||||
if (text === null || text.trim() === '') {
|
||||
text = ' ';
|
||||
} else {
|
||||
text = escape(text);
|
||||
}
|
||||
return '<div>' + text + '</div>';
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
const select = new TomSelect(node, options);
|
||||
node.addEventListener('data-reloaded', (event) => {
|
||||
select.clear(true);
|
||||
|
||||
80
assets/js/forms/KimaiFormTomselectPlugin.js
Normal file
80
assets/js/forms/KimaiFormTomselectPlugin.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiFormPlugin: base class for all none ID plugin that handle forms
|
||||
*/
|
||||
|
||||
import KimaiFormPlugin from './KimaiFormPlugin';
|
||||
|
||||
export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
|
||||
|
||||
/**
|
||||
* @param {string} rendererType
|
||||
* @return array
|
||||
*/
|
||||
getRenderer(rendererType)
|
||||
{
|
||||
// default renderer
|
||||
|
||||
let render = {
|
||||
option_create: (data, escape) => {
|
||||
const name = escape(data.input);
|
||||
if (name.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const tpl = this.translate('select.search.create');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>')
|
||||
return '<div class="create">' + tplReplaced + '</div>';
|
||||
},
|
||||
no_results: (data, escape) => {
|
||||
const tpl = this.translate('select.search.notfound');
|
||||
const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>')
|
||||
return '<div class="no-results">' + tplReplaced + '</div>';
|
||||
},
|
||||
};
|
||||
|
||||
if (rendererType === 'color') {
|
||||
render = {...render, ...{
|
||||
option: function(data, escape) {
|
||||
let item = '<div class="list-group-item border-0 p-1 ps-2 text-nowrap">';
|
||||
// if no color is set, do NOT add an empty placeholder
|
||||
if (data.color !== undefined) {
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||
}
|
||||
item += escape(data.text) + '</div>';
|
||||
return item;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
let item = '<div class="text-nowrap">';
|
||||
// if no color is set, do NOT add an empty placeholder
|
||||
if (data.color !== undefined) {
|
||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||
}
|
||||
item += escape(data.text) + '</div>';
|
||||
return item;
|
||||
}
|
||||
}};
|
||||
} else {
|
||||
render = {...render, ...{
|
||||
// the empty entry would collapse and only show as a tiny 5px line if there is no content inside
|
||||
option: function(data, escape) {
|
||||
let text = data.text;
|
||||
if (text === null || text.trim() === '') {
|
||||
text = ' ';
|
||||
} else {
|
||||
text = escape(text);
|
||||
}
|
||||
return '<div>' + text + '</div>';
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
return render;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,6 @@ export default class KimaiAPI extends KimaiPlugin {
|
||||
|
||||
_headers() {
|
||||
const headers = new Headers();
|
||||
headers.append('X-AUTH-SESSION', '1');
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
return headers;
|
||||
|
||||
@@ -17,9 +17,10 @@ import { Modal } from 'bootstrap';
|
||||
|
||||
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
|
||||
constructor(selector) {
|
||||
constructor(selector, stopSelector) {
|
||||
super();
|
||||
this._selector = selector;
|
||||
this._stopSelector = stopSelector;
|
||||
}
|
||||
|
||||
getId()
|
||||
@@ -63,7 +64,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
|
||||
this.addClickHandler(this._selector, (href) => {
|
||||
this.openUrlInModal(href);
|
||||
});
|
||||
}, this._stopSelector);
|
||||
}
|
||||
|
||||
_getModal()
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class KimaiAlternativeLinks extends KimaiReducedClickHandler {
|
||||
init() {
|
||||
this.addClickHandler(this._selector, function(href) {
|
||||
window.location = href;
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
|
||||
/**
|
||||
* No _underscore naming for now, as it would be mangled otherwise
|
||||
* @param selector
|
||||
* @param callback
|
||||
* @param {string} selector
|
||||
* @param {callback} callback
|
||||
* @param {array<string>} stopSelector
|
||||
*/
|
||||
addClickHandler(selector, callback) {
|
||||
addClickHandler(selector, callback, stopSelector) {
|
||||
document.body.addEventListener('click', (event) => {
|
||||
// event.currentTarget is ALWAYS the body
|
||||
|
||||
@@ -38,6 +39,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x of stopSelector) {
|
||||
if (target.matches(x)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
target = target.parentNode;
|
||||
}
|
||||
|
||||
@@ -54,6 +61,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x of stopSelector) {
|
||||
if (target.matches(x)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ fieldset legend.col-form-label {
|
||||
}
|
||||
|
||||
/* See project assignment to teams */
|
||||
fieldset.form-fieldset legend {
|
||||
fieldset.form-fieldset > legend {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ table.dataTable {
|
||||
}
|
||||
th.weekend,
|
||||
td.weekend {
|
||||
background-color: var(--tblr-bg-surface-secondary);
|
||||
background-color: var(--tblr-gray-50);
|
||||
}
|
||||
/* order is important, "today” should overwrite "weekend" therefor later in the file */
|
||||
th.today {
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
|
||||
@include color-mode(dark, true) {
|
||||
$dark-font-opacity: 0.6;
|
||||
|
||||
/*
|
||||
--tblr-body-bg: #1e222b;
|
||||
--tblr-bg-surface-tertiary: #1e222b;
|
||||
--tblr-bg-surface: #1e222b;
|
||||
--tblr-bg-surface-secondary: #17202b;
|
||||
//--tblr-muted: #d9dcdf;
|
||||
*/
|
||||
|
||||
.table thead th {
|
||||
color: inherit;
|
||||
opacity: $dark-font-opacity;
|
||||
}
|
||||
table.dataTable {
|
||||
th.today {
|
||||
color: var(--tblr-muted);
|
||||
}
|
||||
thead .sorting:after {
|
||||
opacity: $dark-font-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@@ -12,4 +30,16 @@
|
||||
color: var(--tblr-white);
|
||||
}
|
||||
}
|
||||
/* Fix tabs pills (e.g. user profile) */
|
||||
.nav {
|
||||
--tblr-nav-link-color: inherit;
|
||||
opacity: $dark-font-opacity;
|
||||
}
|
||||
.datagrid-title {
|
||||
color: inherit;
|
||||
opacity: $dark-font-opacity;
|
||||
}
|
||||
.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
@@ -35,14 +35,14 @@
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^1.2",
|
||||
"kevinpapst/tabler-bundle": "1.4.2",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
"nelmio/cors-bundle": "^2.0",
|
||||
"onelogin/php-saml": "^4.0",
|
||||
"pagerfanta/pagerfanta": "^3.0",
|
||||
"phpoffice/phpspreadsheet": "^1.16",
|
||||
"phpoffice/phpspreadsheet": "^2.0",
|
||||
"phpoffice/phpword": "^1.0",
|
||||
"psr/container": "^2.0",
|
||||
"psr/log": "^3.0",
|
||||
@@ -84,6 +84,7 @@
|
||||
"fakerphp/faker": "^1.15",
|
||||
"friendsofphp/php-cs-fixer": "^3.3",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||
"phpstan/phpstan-doctrine": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.0",
|
||||
@@ -170,7 +171,6 @@
|
||||
"@tests-integration"
|
||||
],
|
||||
"linting": [
|
||||
"composer validate --strict",
|
||||
"bin/console lint:container",
|
||||
"bin/console lint:yaml config --parse-tags",
|
||||
"bin/console lint:twig templates --show-deprecations",
|
||||
@@ -197,9 +197,6 @@
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.0.x-dev"
|
||||
},
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
|
||||
2941
composer.lock
generated
2941
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -386,8 +386,8 @@
|
||||
],
|
||||
'en_ID' =>
|
||||
[
|
||||
'date' => 'M/d/yy',
|
||||
'time' => 'HH:mm',
|
||||
'date' => 'dd/MM/y',
|
||||
'time' => 'HH.mm',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -603,7 +603,7 @@
|
||||
],
|
||||
'en_NZ' =>
|
||||
[
|
||||
'date' => 'd/MM/yy',
|
||||
'date' => 'dd/MM/y',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
@@ -870,14 +870,14 @@
|
||||
'es_AR' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'es_BO' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -898,7 +898,7 @@
|
||||
'es_CL' =>
|
||||
[
|
||||
'date' => 'dd-MM-yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -912,14 +912,14 @@
|
||||
'es_CR' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'es_CU' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -933,7 +933,7 @@
|
||||
'es_EC' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -954,28 +954,28 @@
|
||||
'es_GT' =>
|
||||
[
|
||||
'date' => 'd/MM/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'es_HN' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'es_MX' =>
|
||||
[
|
||||
'date' => 'dd/MM/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'es_NI' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -989,7 +989,7 @@
|
||||
'es_PE' =>
|
||||
[
|
||||
'date' => 'd/MM/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -1010,14 +1010,14 @@
|
||||
'es_PY' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'es_SV' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -1031,7 +1031,7 @@
|
||||
'es_UY' =>
|
||||
[
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
@@ -1835,7 +1835,7 @@
|
||||
],
|
||||
'sl' =>
|
||||
[
|
||||
'date' => 'd. MM. yy',
|
||||
'date' => 'd. M. yy',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
@@ -1905,14 +1905,14 @@
|
||||
],
|
||||
'vi' =>
|
||||
[
|
||||
'date' => 'dd/MM/y',
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'vi_VN' =>
|
||||
[
|
||||
'date' => 'dd/MM/y',
|
||||
'date' => 'd/M/yy',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
@@ -1931,4 +1931,11 @@
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'zh_Hant_TW' =>
|
||||
[
|
||||
'date' => 'y/M/d',
|
||||
'time' => 'Bh:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
];
|
||||
@@ -22,7 +22,10 @@ doctrine:
|
||||
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
datetime_immutable: App\Doctrine\UTCDateTimeImmutableType
|
||||
orm:
|
||||
controller_resolver:
|
||||
auto_mapping: true
|
||||
auto_generate_proxy_classes: '%kernel.debug%'
|
||||
default_entity_manager: default
|
||||
enable_lazy_ghost_objects: true
|
||||
|
||||
@@ -7,6 +7,8 @@ framework:
|
||||
csrf_protection: true
|
||||
annotations: false
|
||||
handle_all_throwables: true
|
||||
disallow_search_engine_index: true
|
||||
set_content_language_from_locale: true
|
||||
|
||||
serializer:
|
||||
enable_attributes: true
|
||||
|
||||
@@ -105,21 +105,22 @@ kimai:
|
||||
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||
LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet']
|
||||
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
|
||||
EVERYONE: ['api_access']
|
||||
# permissions which are deactivated, as these features are hidden for now
|
||||
# brave users can try to activate them and be surprised what happens
|
||||
REGISTER_BETA: []
|
||||
# mapping a "role name" to an array of "set names"
|
||||
maps:
|
||||
ROLE_USER: ['TIMESHEET','PROFILE']
|
||||
ROLE_TEAMLEAD: ['ACTIVITIES_TEAMLEAD','PROJECTS_TEAMLEAD','CUSTOMERS_TEAMLEAD','TIMESHEET_OTHER','INVOICE','TIMESHEET','PROFILE','EXPORT','BILLABLE','TAGS','REPORTING']
|
||||
ROLE_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING']
|
||||
ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING']
|
||||
ROLE_USER: ['TIMESHEET','PROFILE', 'EVERYONE']
|
||||
ROLE_TEAMLEAD: ['ACTIVITIES_TEAMLEAD','PROJECTS_TEAMLEAD','CUSTOMERS_TEAMLEAD','TIMESHEET_OTHER','INVOICE','TIMESHEET','PROFILE','EXPORT','BILLABLE','TAGS','REPORTING', 'EVERYONE']
|
||||
ROLE_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE']
|
||||
ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE']
|
||||
# mapping a "role name" to an array of "permission names"
|
||||
roles:
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile']
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting','hours_own_profile']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile','hours_own_profile']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile','hours_own_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile','hours_own_profile']
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -19,12 +19,6 @@ when@prod:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!deprecation"]
|
||||
# deactivated, because currently there are too many deprecations cause by gedmo and doctrine
|
||||
# deprecation:
|
||||
# type: stream
|
||||
# channels: ["deprecation"]
|
||||
# path: "%kernel.logs_dir%/deprecations.log"
|
||||
# formatter: monolog.formatter.deprecation
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
|
||||
@@ -40,27 +40,23 @@ nelmio_api_doc:
|
||||
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
||||
- { alias: TeamMember, type: App\Entity\TeamMember, groups: [Team_Entity] }
|
||||
- { alias: TeamMembership, type: App\Entity\TeamMember, groups: [User_Entity] }
|
||||
- { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] }
|
||||
- { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] }
|
||||
areas:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
documentation:
|
||||
info:
|
||||
title: Kimai - API Docs
|
||||
title: Kimai - API
|
||||
description: |
|
||||
JSON API for the Kimai time-tracking software: [API documentation](https://www.kimai.org/documentation/rest-api.html), [Swagger definition file](doc.json)
|
||||
version: '0.7'
|
||||
JSON API for the Kimai time-tracking software. Read our [API documentation](https://www.kimai.org/documentation/rest-api.html) and download the [Open API definition](doc.json) to import into your API client.
|
||||
version: '1.0'
|
||||
components:
|
||||
securitySchemes:
|
||||
apiUser:
|
||||
type: apiKey
|
||||
description: 'Value: {Username}'
|
||||
name: X-AUTH-USER
|
||||
in: header
|
||||
apiToken:
|
||||
type: apiKey
|
||||
description: 'Value: {API Token}'
|
||||
name: X-AUTH-TOKEN
|
||||
in: header
|
||||
bearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: KIMAI
|
||||
description: API Token
|
||||
security:
|
||||
- X-AUTH-USER: []
|
||||
X-AUTH-TOKEN: []
|
||||
- bearer: []
|
||||
|
||||
@@ -17,9 +17,10 @@ nelmio_cors:
|
||||
hosts: []
|
||||
origin_regex: true
|
||||
forced_allow_origin_value: ~
|
||||
# allow_private_network: true
|
||||
paths:
|
||||
'^/api/':
|
||||
allow_origin: ['*']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type', 'Authorization']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
max_age: 3600
|
||||
|
||||
7
config/packages/rate_limiter.yaml
Normal file
7
config/packages/rate_limiter.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
framework:
|
||||
rate_limiter:
|
||||
session_prediction:
|
||||
policy: 'fixed_window'
|
||||
limit: 250
|
||||
interval: '1 hour'
|
||||
lock_factory: null
|
||||
@@ -18,6 +18,10 @@ security:
|
||||
security: false
|
||||
|
||||
api:
|
||||
access_token:
|
||||
token_handler: App\API\Authentication\AccessTokenHandler
|
||||
success_handler: App\API\Authentication\AccessTokenSuccessHandler
|
||||
remember_me: false
|
||||
request_matcher: App\API\Authentication\ApiRequestMatcher
|
||||
user_checker: App\Security\UserChecker
|
||||
stateless: true
|
||||
@@ -35,7 +39,6 @@ security:
|
||||
entry_point: form_login
|
||||
|
||||
custom_authenticators:
|
||||
- App\API\Authentication\SessionAuthenticator
|
||||
- App\Saml\SamlAuthenticator
|
||||
|
||||
remember_me:
|
||||
@@ -60,7 +63,7 @@ security:
|
||||
logout:
|
||||
path: logout
|
||||
target: homepage
|
||||
enable_csrf: true
|
||||
enable_csrf: false
|
||||
|
||||
login_throttling:
|
||||
max_attempts: 5
|
||||
@@ -69,10 +72,11 @@ security:
|
||||
login_link:
|
||||
check_route: link_login_check
|
||||
signature_properties: ['id']
|
||||
lifetime: 300
|
||||
max_uses: 1
|
||||
lifetime: 900
|
||||
max_uses: 3
|
||||
|
||||
access_decision_manager:
|
||||
# only grants access if there is no voter denying access
|
||||
strategy: unanimous
|
||||
allow_if_all_abstain: false
|
||||
|
||||
@@ -85,13 +89,13 @@ security:
|
||||
access_control:
|
||||
- { path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
|
||||
- { path: '^/auth', roles: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)$', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/auth', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/login', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/register', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/resetting', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED }
|
||||
- { path: '^/{_locale}$', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/auth', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/login', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/register', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/resetting', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED }
|
||||
|
||||
when@test:
|
||||
# this configuration simplifies testing URLs protected by the security mechanism
|
||||
@@ -104,7 +108,3 @@ when@test:
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
|
||||
firewalls:
|
||||
secured_area:
|
||||
http_basic: ~
|
||||
|
||||
@@ -29,7 +29,6 @@ tabler:
|
||||
routes:
|
||||
tabler_welcome: dashboard
|
||||
tabler_login: login
|
||||
tabler_logout: logout
|
||||
tabler_login_check: security_check
|
||||
tabler_registration: registration_register
|
||||
tabler_registration_register: registration_register
|
||||
@@ -122,7 +121,7 @@ tabler:
|
||||
search: fas fa-search
|
||||
settings: fas fa-cog
|
||||
shop: fas fa-shopping-cart
|
||||
spinner: fas fa-spinner fa-pulse
|
||||
spinner: fas fa-spinner
|
||||
start: fas fa-play
|
||||
start-small: far fa-play-circle
|
||||
stop: fas fa-stop
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
framework:
|
||||
# https://symfony.com/doc/current/reference/configuration/framework.html#enabled-locales
|
||||
enabled_locales: '%kimai_locales%'
|
||||
default_locale: '%locale%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
|
||||
@@ -2,8 +2,6 @@ controllers:
|
||||
resource: ../src/Controller/
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -31,8 +29,6 @@ security:
|
||||
resource: ../src/Controller/Security/
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -49,8 +45,6 @@ home:
|
||||
|
||||
homeLocale:
|
||||
path: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /{_locale}/_error
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
app.test.active_entries:
|
||||
path: /{_locale}/layou/active_entries
|
||||
controller: App\Controller\LayoutController::activeEntries
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
parameters:
|
||||
locale: en
|
||||
# can be regenerated with "bin/console kimai:reset:locales"
|
||||
app_locales: ar|cs|da|de|de_CH|el|en|eo|es|eu|fa|fi|fo|fr|he|hr|hu|id|it|ja|ko|nb_NO|nl|pa|pl|pt|pt_BR|ro|ru|sk|sl|sv|tr|uk|vi|zh_CN|zh_Hant|cs_CZ|da_DK|da_GL|de_AT|de_BE|de_CH|de_DE|de_IT|de_LI|de_LU|el_CY|el_GR|en_AE|en_AG|en_AI|en_AS|en_AT|en_AU|en_BB|en_BE|en_BI|en_BM|en_BS|en_BW|en_BZ|en_CA|en_CC|en_CH|en_CK|en_CM|en_CX|en_CY|en_DE|en_DG|en_DK|en_DM|en_ER|en_FI|en_FJ|en_FK|en_FM|en_GB|en_GD|en_GG|en_GH|en_GI|en_GM|en_GU|en_GY|en_HK|en_ID|en_IE|en_IL|en_IM|en_IN|en_IO|en_JE|en_JM|en_KE|en_KI|en_KN|en_KY|en_LC|en_LR|en_LS|en_MG|en_MH|en_MO|en_MP|en_MS|en_MT|en_MU|en_MV|en_MW|en_MY|en_NA|en_NF|en_NG|en_NH|en_NL|en_NR|en_NU|en_NZ|en_PG|en_PH|en_PK|en_PN|en_PR|en_PW|en_RH|en_RW|en_SB|en_SC|en_SD|en_SE|en_SG|en_SH|en_SI|en_SL|en_SS|en_SX|en_SZ|en_TC|en_TK|en_TO|en_TT|en_TV|en_TZ|en_UG|en_UM|en_US|en_VC|en_VG|en_VI|en_VU|en_WS|en_ZA|en_ZM|en_ZW|es_AR|es_BO|es_BR|es_BZ|es_CL|es_CO|es_CR|es_CU|es_DO|es_EA|es_EC|es_ES|es_GQ|es_GT|es_HN|es_IC|es_MX|es_NI|es_PA|es_PE|es_PH|es_PR|es_PY|es_SV|es_US|es_UY|es_VE|eu_ES|fa_AF|fa_IR|fi_FI|fo_DK|fo_FO|fr_BE|fr_BF|fr_BI|fr_BJ|fr_BL|fr_CA|fr_CD|fr_CF|fr_CG|fr_CH|fr_CI|fr_CM|fr_DJ|fr_DZ|fr_FR|fr_GA|fr_GF|fr_GN|fr_GP|fr_GQ|fr_HT|fr_KM|fr_LU|fr_MA|fr_MC|fr_MF|fr_MG|fr_ML|fr_MQ|fr_MR|fr_MU|fr_NC|fr_NE|fr_PF|fr_PM|fr_RE|fr_RW|fr_SC|fr_SN|fr_SY|fr_TD|fr_TG|fr_TN|fr_VU|fr_WF|fr_YT|he_IL|hr_BA|hr_HR|hu_HU|it_CH|it_IT|it_SM|it_VA|ja_JP|ko_CN|ko_KP|ko_KR|nl_AW|nl_BE|nl_BQ|nl_CW|nl_NL|nl_SR|nl_SX|pl_PL|pt_AO|pt_BR|pt_CH|pt_CV|pt_GQ|pt_GW|pt_LU|pt_MO|pt_MZ|pt_PT|pt_ST|pt_TL|ro_MD|ro_RO|ru_BY|ru_KG|ru_KZ|ru_MD|ru_RU|ru_UA|sk_SK|sv_AX|sv_FI|sv_SE|tr_CY|tr_TR|uk_UA|vi_VN
|
||||
# can be regenerated with: bin/console kimai:reset:locales
|
||||
kimai_locales: ['ar', 'ca', 'cs', 'da', 'de', 'de_CH', 'el', 'en', 'eo', 'es', 'eu', 'fa', 'fi', 'fo', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'nb_NO', 'nl', 'pa', 'pl', 'pt', 'pt_BR', 'ro', 'ru', 'sk', 'sl', 'sv', 'tr', 'uk', 'vi', 'zh_CN', 'zh_Hant', 'zh_Hant_TW', 'ca_AD', 'ca_ES', 'ca_FR', 'ca_IT', 'cs_CZ', 'da_DK', 'da_GL', 'de_AT', 'de_BE', 'de_CH', 'de_DE', 'de_IT', 'de_LI', 'de_LU', 'el_CY', 'el_GR', 'en_AE', 'en_AG', 'en_AI', 'en_AS', 'en_AT', 'en_AU', 'en_BB', 'en_BE', 'en_BI', 'en_BM', 'en_BS', 'en_BW', 'en_BZ', 'en_CA', 'en_CC', 'en_CH', 'en_CK', 'en_CM', 'en_CX', 'en_CY', 'en_DE', 'en_DG', 'en_DK', 'en_DM', 'en_ER', 'en_FI', 'en_FJ', 'en_FK', 'en_FM', 'en_GB', 'en_GD', 'en_GG', 'en_GH', 'en_GI', 'en_GM', 'en_GU', 'en_GY', 'en_HK', 'en_ID', 'en_IE', 'en_IL', 'en_IM', 'en_IN', 'en_IO', 'en_JE', 'en_JM', 'en_KE', 'en_KI', 'en_KN', 'en_KY', 'en_LC', 'en_LR', 'en_LS', 'en_MG', 'en_MH', 'en_MO', 'en_MP', 'en_MS', 'en_MT', 'en_MU', 'en_MV', 'en_MW', 'en_MY', 'en_NA', 'en_NF', 'en_NG', 'en_NH', 'en_NL', 'en_NR', 'en_NU', 'en_NZ', 'en_PG', 'en_PH', 'en_PK', 'en_PN', 'en_PR', 'en_PW', 'en_RH', 'en_RW', 'en_SB', 'en_SC', 'en_SD', 'en_SE', 'en_SG', 'en_SH', 'en_SI', 'en_SL', 'en_SS', 'en_SX', 'en_SZ', 'en_TC', 'en_TK', 'en_TO', 'en_TT', 'en_TV', 'en_TZ', 'en_UG', 'en_UM', 'en_US', 'en_VC', 'en_VG', 'en_VI', 'en_VU', 'en_WS', 'en_ZA', 'en_ZM', 'en_ZW', 'es_AR', 'es_BO', 'es_BR', 'es_BZ', 'es_CL', 'es_CO', 'es_CR', 'es_CU', 'es_DO', 'es_EA', 'es_EC', 'es_ES', 'es_GQ', 'es_GT', 'es_HN', 'es_IC', 'es_MX', 'es_NI', 'es_PA', 'es_PE', 'es_PH', 'es_PR', 'es_PY', 'es_SV', 'es_US', 'es_UY', 'es_VE', 'eu_ES', 'fa_AF', 'fa_IR', 'fi_FI', 'fo_DK', 'fo_FO', 'fr_BE', 'fr_BF', 'fr_BI', 'fr_BJ', 'fr_BL', 'fr_CA', 'fr_CD', 'fr_CF', 'fr_CG', 'fr_CH', 'fr_CI', 'fr_CM', 'fr_DJ', 'fr_DZ', 'fr_FR', 'fr_GA', 'fr_GF', 'fr_GN', 'fr_GP', 'fr_GQ', 'fr_HT', 'fr_KM', 'fr_LU', 'fr_MA', 'fr_MC', 'fr_MF', 'fr_MG', 'fr_ML', 'fr_MQ', 'fr_MR', 'fr_MU', 'fr_NC', 'fr_NE', 'fr_PF', 'fr_PM', 'fr_RE', 'fr_RW', 'fr_SC', 'fr_SN', 'fr_SY', 'fr_TD', 'fr_TG', 'fr_TN', 'fr_VU', 'fr_WF', 'fr_YT', 'he_IL', 'hr_BA', 'hr_HR', 'hu_HU', 'it_CH', 'it_IT', 'it_SM', 'it_VA', 'ja_JP', 'ko_CN', 'ko_KP', 'ko_KR', 'nl_AW', 'nl_BE', 'nl_BQ', 'nl_CW', 'nl_NL', 'nl_SR', 'nl_SX', 'pl_PL', 'pt_AO', 'pt_BR', 'pt_CH', 'pt_CV', 'pt_GQ', 'pt_GW', 'pt_LU', 'pt_MO', 'pt_MZ', 'pt_PT', 'pt_ST', 'pt_TL', 'ro_MD', 'ro_RO', 'ru_BY', 'ru_KG', 'ru_KZ', 'ru_MD', 'ru_RU', 'ru_UA', 'sk_SK', 'sv_AX', 'sv_FI', 'sv_SE', 'tr_CY', 'tr_TR', 'uk_UA', 'vi_VN']
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -23,12 +23,14 @@ services:
|
||||
- '../src/API/Model/'
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Event/'
|
||||
- '../src/Form/Model/'
|
||||
- '../src/Model/'
|
||||
- '../src/Repository/Loader/'
|
||||
- '../src/Repository/Paginator/'
|
||||
- '../src/Repository/Query/'
|
||||
- '../src/Repository/Result/'
|
||||
- '../src/Event/'
|
||||
- '../src/Model/'
|
||||
- '../src/WorkingTime/Calculator/'
|
||||
- '../src/Kernel.php'
|
||||
- '../src/Constants.php'
|
||||
|
||||
@@ -100,7 +102,9 @@ services:
|
||||
# SAML Services
|
||||
# ================================================================================
|
||||
App\Saml\SamlProvider:
|
||||
arguments: ['@App\Repository\UserRepository', '@security.user.provider.concrete.kimai_internal', '@App\Configuration\SamlConfigurationInterface']
|
||||
arguments:
|
||||
$userProvider: '@security.user.provider.concrete.kimai_internal'
|
||||
$configuration: '@App\Configuration\SamlConfigurationInterface'
|
||||
|
||||
# ================================================================================
|
||||
# REPOSITORIES
|
||||
@@ -195,6 +199,11 @@ services:
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\WorkingTime']
|
||||
|
||||
App\Repository\AccessTokenRepository:
|
||||
class: App\Repository\AccessTokenRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\AccessToken']
|
||||
|
||||
monolog.formatter.kimai:
|
||||
class: Monolog\Formatter\LineFormatter
|
||||
arguments:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: ar|cs|de|de_CH|da|en|eo|es|eu|fr|he|hu|it|ja|ko|nl|pl|pt_BR|ro|ru|sk|sv|tr|vi|zh_CN
|
||||
kimai_locales: ['ar', 'cs', 'de', 'de_CH', 'da', 'en', 'eo', 'es', 'eu', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pl', 'pt_BR', 'ro', 'ru', 'sk', 'sv', 'tr', 'vi', 'zh_CN']
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
@@ -23,7 +23,3 @@ services:
|
||||
App\User\UserService:
|
||||
public: true
|
||||
arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.user_password_hasher']
|
||||
|
||||
App\Plugin\PluginManager:
|
||||
public: true
|
||||
arguments: [!tagged kimai.plugin]
|
||||
|
||||
@@ -24,11 +24,15 @@ final class <className> extends AbstractMigration
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->abortIf(true, 'Migration was auto-generated, adapt to your needs before running it.');
|
||||
|
||||
<up>
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->abortIf(true, 'Migration was auto-generated, adapt to your needs before running it.');
|
||||
|
||||
<down>
|
||||
}<override>
|
||||
}
|
||||
|
||||
56
migrations/Version20240214061246.php
Normal file
56
migrations/Version20240214061246.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.14
|
||||
*/
|
||||
final class Version20240214061246 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds the table for API access tokens';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$accessTokens = $schema->createTable('kimai2_access_token');
|
||||
|
||||
$accessTokens->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$accessTokens->addColumn('user_id', 'integer', ['notnull' => true]);
|
||||
$accessTokens->addColumn('token', 'string', ['notnull' => true, 'length' => 100]);
|
||||
$accessTokens->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$accessTokens->addColumn('last_usage', 'datetime_immutable', ['notnull' => false, 'default' => null]);
|
||||
$accessTokens->addColumn('expires_at', 'datetime_immutable', ['notnull' => false, 'default' => null]);
|
||||
|
||||
$accessTokens->setPrimaryKey(['id']);
|
||||
|
||||
$accessTokens->addIndex(['user_id'], 'IDX_6FB0DB1EA76ED395');
|
||||
$accessTokens->addUniqueIndex(['token'], 'UNIQ_6FB0DB1E5F37A13B');
|
||||
|
||||
$accessTokens->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_6FB0DB1EA76ED395');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$table = $schema->getTable('kimai2_access_token');
|
||||
$table->removeForeignKey('FK_6FB0DB1EA76ED395');
|
||||
|
||||
$schema->dropTable('kimai2_access_token');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
36
migrations/Version20240326125247.php
Normal file
36
migrations/Version20240326125247.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.14
|
||||
*/
|
||||
final class Version20240326125247 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds the number columns to activity and project';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE kimai2_activities ADD number VARCHAR(10) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE kimai2_projects ADD number VARCHAR(10) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE kimai2_projects DROP number');
|
||||
$this->addSql('ALTER TABLE kimai2_activities DROP number');
|
||||
}
|
||||
}
|
||||
61
migrations/Version20240920105524.php
Normal file
61
migrations/Version20240920105524.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.22
|
||||
*/
|
||||
final class Version20240920105524 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Updates the user preferences for configurable work contract type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$ids = $this->connection->fetchFirstColumn("
|
||||
SELECT DISTINCT user_id
|
||||
FROM kimai2_user_preferences AS kp
|
||||
WHERE kp.value > 0
|
||||
AND kp.name IN ('work_monday', 'work_tuesday', 'work_wednesday', 'work_thursday', 'work_friday', 'work_saturday', 'work_sunday')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM kimai2_user_preferences AS kp2
|
||||
WHERE kp2.user_id = kp.user_id
|
||||
AND kp2.name = 'work_contract_type'
|
||||
);");
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$this->addSql('INSERT INTO kimai2_user_preferences (`user_id`, `name`, `value`) VALUES (:id, :name, :value)', [
|
||||
'id' => $id,
|
||||
'name' => 'work_contract_type',
|
||||
'value' => 'day',
|
||||
]);
|
||||
}
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
$this->preventEmptyMigrationWarning();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql("DELETE FROM kimai2_user_preferences WHERE `name` = 'work_contract_type'");
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
16
package.json
16
package.json
@@ -24,10 +24,10 @@
|
||||
],
|
||||
"packageManager": "yarn@3.2.1",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
"@babel/preset-env": "^7.19.4",
|
||||
"@fortawesome/fontawesome-free": "^6.5.0",
|
||||
"@babel/core": "^7.19",
|
||||
"@babel/eslint-parser": "^7.19",
|
||||
"@babel/preset-env": "^7.19",
|
||||
"@fortawesome/fontawesome-free": "^6.5",
|
||||
"@fullcalendar/bootstrap5": "^5.11",
|
||||
"@fullcalendar/core": "^5.11",
|
||||
"@fullcalendar/daygrid": "^5.11",
|
||||
@@ -37,17 +37,17 @@
|
||||
"@fullcalendar/timegrid": "^5.11",
|
||||
"@symfony/webpack-encore": "^4.0",
|
||||
"@tabler/core": "1.0.0-beta19",
|
||||
"chart.js": "^3.9",
|
||||
"chart.js": "^4.0",
|
||||
"core-js": "^3.6",
|
||||
"eslint": "^8.0",
|
||||
"eslint-webpack-plugin": "^3.0",
|
||||
"gridstack": "^7.1",
|
||||
"litepicker": "^2.0",
|
||||
"luxon": "^3.3",
|
||||
"sass": "^1.62.1",
|
||||
"sass": "^1.62",
|
||||
"sass-loader": "^13.2",
|
||||
"tom-select": "^2.2",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0"
|
||||
"webpack": "^5.91",
|
||||
"webpack-cli": "^4.10"
|
||||
}
|
||||
}
|
||||
|
||||
926
phpstan.neon
926
phpstan.neon
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
# See https://github.com/symfony/recipes-contrib/blob/main/symfony/apache-pack/1.0/public/.htaccess#L38
|
||||
|
||||
# Use the front controller as index file. It serves as a fallback solution when
|
||||
# every other rewrite/redirect fails (e.g. in an aliased environment without
|
||||
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||
@@ -5,30 +7,46 @@
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex index.php
|
||||
|
||||
# Do NOT use Kimai with http, this is a huge security risk!
|
||||
# Activate these lines if you configured SSL and certificates:
|
||||
# By default, Apache does not evaluate symbolic links if you did not enable this
|
||||
# feature in your server configuration. Uncomment the following line if you
|
||||
# install assets as symlinks or if you experience problems related to symlinks
|
||||
# when compiling LESS/Sass/CoffeScript assets.
|
||||
# Options +SymLinksIfOwnerMatch
|
||||
|
||||
#<IfModule mod_rewrite.c>
|
||||
# RewriteEngine On
|
||||
# RewriteCond %{SERVER_PORT} 80
|
||||
# RewriteRule ^(.*)$ https://kimai.example.com/$1 [R,L]
|
||||
#</IfModule>
|
||||
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
|
||||
# to the front controller "/index.php" but be rewritten to "/index.php/index".
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
# This Option needs to be enabled for RewriteRule, otherwise it will show an error like
|
||||
# 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'
|
||||
Options +SymLinksIfOwnerMatch
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Determine the RewriteBase automatically and set it as environment variable.
|
||||
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||
# project in a subdirectory, the base path will be prepended to allow proper
|
||||
# resolution of the app.php file and to redirect to the correct URI. It will
|
||||
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||
# work in environments without path prefix as well, providing a safe, one-size
|
||||
# fits all solution. But as you do not need it in this case, you can comment
|
||||
# the following 2 lines to eliminate the overhead.
|
||||
RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
|
||||
RewriteRule ^(.*) - [E=BASE:%1]
|
||||
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||
RewriteRule .* - [E=BASE:%1]
|
||||
|
||||
# Sets the HTTP_AUTHORIZATION header removed by Apache, if PHP is installed as CGI
|
||||
# Required for the API and below are 3 different methods to achieve it
|
||||
RewriteCond %{HTTP:Authorization} .+
|
||||
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||
# This might be the only required line
|
||||
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||
# Apache 2.4.13+
|
||||
# CGIPassAuth On
|
||||
|
||||
# Redirect to URI without front controller to prevent duplicate content
|
||||
# (with and without `/app.php`). Only do this redirect on the initial
|
||||
# (with and without `/index.php`). Only do this redirect on the initial
|
||||
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
|
||||
# endless redirect loop (request -> rewrite to front controller ->
|
||||
# redirect -> request -> ...).
|
||||
@@ -38,16 +56,14 @@ DirectoryIndex index.php
|
||||
# - disable this feature by commenting the following 2 lines or
|
||||
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||
# following RewriteCond (best solution)
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} ^$
|
||||
RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||
|
||||
# If the requested filename exists, simply serve it.
|
||||
# We only want to let Apache serve files and not directories.
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule .? - [L]
|
||||
|
||||
# Rewrite all other queries to the front controller.
|
||||
RewriteRule .? %{ENV:BASE}/index.php [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
@@ -55,7 +71,7 @@ DirectoryIndex index.php
|
||||
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||
# the start page to the front controller explicitly so that the website
|
||||
# and the generated links can still be used.
|
||||
RedirectMatch 302 ^/$ /index.php/
|
||||
RedirectMatch 307 ^/$ /index.php/
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[535],{1548:function(i,n,u){u(846)},846:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=1548,i(i.s=n)}]);
|
||||
File diff suppressed because one or more lines are too long
1
public/build/app-rtl.97153087.js
Normal file
1
public/build/app-rtl.97153087.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[113],{2395:function(i,n,u){u(3865)},3865:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=2395,i(i.s=n)}]);
|
||||
2
public/build/app.3611eec9.js
Normal file
2
public/build/app.3611eec9.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/calendar.e52c9d34.js
Normal file
2
public/build/calendar.e52c9d34.js
Normal file
File diff suppressed because one or more lines are too long
2
public/build/chart.80ee190c.js
Normal file
2
public/build/chart.80ee190c.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,13 +1,13 @@
|
||||
/*!
|
||||
* @kurkle/color v0.2.1
|
||||
* @kurkle/color v0.3.2
|
||||
* https://github.com/kurkle/color#readme
|
||||
* (c) 2022 Jukka Kurkela
|
||||
* (c) 2023 Jukka Kurkela
|
||||
* Released under the MIT License
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Chart.js v3.9.1
|
||||
* Chart.js v4.4.2
|
||||
* https://www.chartjs.org
|
||||
* (c) 2022 Chart.js Contributors
|
||||
* (c) 2024 Chart.js Contributors
|
||||
* Released under the MIT License
|
||||
*/
|
||||
File diff suppressed because one or more lines are too long
2
public/build/dashboard.10118fb0.js
Normal file
2
public/build/dashboard.10118fb0.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,26 +2,26 @@
|
||||
"entrypoints": {
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/app.6450be5e.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/app.3611eec9.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.3501ccee.css"
|
||||
"/build/app.c4fc7cc8.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/app-rtl.3d445498.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/app-rtl.97153087.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.670658da.css"
|
||||
"/build/app-rtl.83358c8a.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/export-pdf.d367a32e.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/export-pdf.1442bee7.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/export-pdf.d8a6c23b.css"
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"invoice": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/invoice.2604495e.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/invoice.7ef8a0c8.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/invoice.5bea118e.css"
|
||||
@@ -38,8 +38,8 @@
|
||||
},
|
||||
"invoice-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/invoice-pdf.ce9aace0.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/invoice-pdf.6091a528.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/invoice-pdf.2b749265.css"
|
||||
@@ -47,14 +47,14 @@
|
||||
},
|
||||
"chart": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/chart.f5becfac.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/chart.80ee190c.js"
|
||||
]
|
||||
},
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/calendar.d45e4af5.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/calendar.e52c9d34.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.bb473428.css"
|
||||
@@ -62,8 +62,8 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/dashboard.6774a712.js"
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/dashboard.10118fb0.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/dashboard.18f5a8b7.css"
|
||||
@@ -71,21 +71,21 @@
|
||||
}
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||
"/build/app.6450be5e.js": "sha384-P5Qc8Rav736nZcS2vqM2rBRZpr3bwMsk4SJENBsOMDPdfEjDX7M8WMpwhjzxj0PQ",
|
||||
"/build/app.3501ccee.css": "sha384-3chwmsldxvcA6ldtsAftB9R2/bSNCi17vSW9ycC0FqLNNM+IdnMorRBXh/zTW1NF",
|
||||
"/build/app-rtl.3d445498.js": "sha384-D5VYIVdHCPGzAPnMAIkRN1Ri9H4DxdwGts1BgG40JXJuOamq6UQXblaDBSpszOZ0",
|
||||
"/build/app-rtl.670658da.css": "sha384-Mo5HQ+W9EWDGM1gOyEZ7aScXNltzBMQa4cWC1HtShvs4XPMNUwzOb97jcXf34qSD",
|
||||
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
|
||||
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
|
||||
"/build/app.3611eec9.js": "sha384-uBLSZpUnOLWU2TLPGZ0gZDqmiAEa2xrKhWVkHheU+vBjcStIq8ZQg3/iqxLS9Su0",
|
||||
"/build/app.c4fc7cc8.css": "sha384-icbl3mA5CyeJXjF1YzLObStpJHF5qZ5GdqALtvDYN4KPOCqREwGdFWDu27GYkI+U",
|
||||
"/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr",
|
||||
"/build/app-rtl.83358c8a.css": "sha384-eKnlBDnocNJ68nIc2l4AWCLn3oUEEbsRJoZ1TUP3mii+ToGt9xs08ih8wEmvO+Dq",
|
||||
"/build/export-pdf.1442bee7.js": "sha384-C6agvjJnQUsMCxaZ/J7dUZU3cpC7uHKI9ZtnfGpRqYbjyw91YLy1oV3iNYgwYyCF",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",
|
||||
"/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG",
|
||||
"/build/invoice.5bea118e.css": "sha384-KNyGdZSTXnAi8Tn2WqrxJnCP18yRK7BrZTHKCAGAEtl5/4S/XAe4IHKpM5gfsmB7",
|
||||
"/build/invoice-pdf.ce9aace0.js": "sha384-mtZkXrpsXFMX2YvmmTlCm91QgsTlZLzLJnDAFcTuBDtDZ7TNkqLYkfCeLzFLVfwb",
|
||||
"/build/invoice-pdf.6091a528.js": "sha384-yJz/MDBgBpNHNxF7DuSyFbJN9EL7oDJjraoPRB7VYlHMdb1T0ZfTcI0L8VMDB46U",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
||||
"/build/calendar.d45e4af5.js": "sha384-wi3/ek3Msn+rk9jebeJnzEXaHUUWlzxkOZwhZyJP5Cg+AKWtolWAXD/hCXQAg0kq",
|
||||
"/build/chart.80ee190c.js": "sha384-KrQszq859PR0u+KHpGy+RO6K1rcMqCFZx+sNiLVNVZHwEi7RwTXqnYbu8zvfWzUI",
|
||||
"/build/calendar.e52c9d34.js": "sha384-a6P/nyvUkmPZglNqIfSKEyon791oFKB2+zgfUZHpy1awKi/FBchr5TqeRYupRDQn",
|
||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
||||
"/build/dashboard.10118fb0.js": "sha384-pasZR1GYf8/7lMFS9OrsxX8nJ9tSlRYr32fWKUhGoF2mNAGrokLEWdgtbYMRHgFB",
|
||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||
}
|
||||
}
|
||||
1
public/build/export-pdf.1442bee7.js
Normal file
1
public/build/export-pdf.1442bee7.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[872],{878:function(i,n,u){u(6154)},6154:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=878,i(i.s=n)}]);
|
||||
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[185],{8810:function(i,n,u){u(9508)},9508:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=8810,i(i.s=n)}]);
|
||||
BIN
public/build/fonts/fa-brands-400.455ea818.woff2
Normal file
BIN
public/build/fonts/fa-brands-400.455ea818.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/build/fonts/fa-regular-400.21cb8f55.woff2
Normal file
BIN
public/build/fonts/fa-regular-400.21cb8f55.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/build/fonts/fa-solid-900.4d986b00.woff2
Normal file
BIN
public/build/fonts/fa-solid-900.4d986b00.woff2
Normal file
Binary file not shown.
Binary file not shown.
1
public/build/invoice-pdf.6091a528.js
Normal file
1
public/build/invoice-pdf.6091a528.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[117],{1555:function(i,n,u){u(8837)},8837:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=1555,i(i.s=n)}]);
|
||||
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[266],{35:function(i,n,u){u(6197)},6197:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=35,i(i.s=n)}]);
|
||||
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[149],{474:function(i,n,u){u(9822)},9822:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=474,i(i.s=n)}]);
|
||||
1
public/build/invoice.7ef8a0c8.js
Normal file
1
public/build/invoice.7ef8a0c8.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[896],{4820:function(i,n,u){u(5650)},5650:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=4820,i(i.s=n)}]);
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"build/app.css": "/build/app.3501ccee.css",
|
||||
"build/app.js": "/build/app.6450be5e.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.670658da.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.3d445498.js",
|
||||
"build/app.css": "/build/app.c4fc7cc8.css",
|
||||
"build/app.js": "/build/app.3611eec9.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.83358c8a.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.97153087.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
|
||||
"build/export-pdf.js": "/build/export-pdf.1442bee7.js",
|
||||
"build/invoice.css": "/build/invoice.5bea118e.css",
|
||||
"build/invoice.js": "/build/invoice.2604495e.js",
|
||||
"build/invoice.js": "/build/invoice.7ef8a0c8.js",
|
||||
"build/invoice-pdf.css": "/build/invoice-pdf.2b749265.css",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.ce9aace0.js",
|
||||
"build/chart.js": "/build/chart.f5becfac.js",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.6091a528.js",
|
||||
"build/chart.js": "/build/chart.80ee190c.js",
|
||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||
"build/calendar.js": "/build/calendar.d45e4af5.js",
|
||||
"build/calendar.js": "/build/calendar.e52c9d34.js",
|
||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
||||
"build/runtime.js": "/build/runtime.f0079159.js",
|
||||
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.bb5be820.ttf",
|
||||
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.4c05cd9b.ttf",
|
||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.4cbe3db2.woff2",
|
||||
"build/fonts/fa-brands-400.woff2": "/build/fonts/fa-brands-400.498f4d22.woff2",
|
||||
"build/fonts/fa-regular-400.ttf": "/build/fonts/fa-regular-400.96e19e1a.ttf",
|
||||
"build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.83824cd5.woff2"
|
||||
"build/dashboard.js": "/build/dashboard.10118fb0.js",
|
||||
"build/runtime.js": "/build/runtime.74179306.js",
|
||||
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.bacd5de6.ttf",
|
||||
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.60127e35.ttf",
|
||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.4d986b00.woff2",
|
||||
"build/fonts/fa-brands-400.woff2": "/build/fonts/fa-brands-400.455ea818.woff2",
|
||||
"build/fonts/fa-regular-400.ttf": "/build/fonts/fa-regular-400.eb91f7b9.ttf",
|
||||
"build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.21cb8f55.woff2"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every((function(e){return t.O[e](n[a])}))?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={666:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some((function(r){return 0!==e[r]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai2=self.webpackChunkkimai2||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();
|
||||
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every((function(e){return t.O[e](n[a])}))?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={121:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some((function(r){return 0!==e[r]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai2=self.webpackChunkkimai2||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();
|
||||
@@ -4,6 +4,9 @@ use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
// TODO remove once PARTIAL usage was replaced entirely
|
||||
\Doctrine\Deprecations\Deprecation::ignoreDeprecations('https://github.com/doctrine/orm/issues/8471');
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ use App\Event\PageActionsEvent;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -27,7 +26,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/actions')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('API')]
|
||||
#[OA\Tag(name: 'Actions')]
|
||||
final class ActionsController extends BaseApiController
|
||||
{
|
||||
@@ -73,8 +72,6 @@ final class ActionsController extends BaseApiController
|
||||
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
|
||||
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
|
||||
#[Route(methods: ['GET'], path: '/timesheet/{id}/{view}/{locale}', name: 'get_timesheet_actions', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function getTimesheetActions(Timesheet $timesheet, string $view, string $locale): Response
|
||||
{
|
||||
$event = new PageActionsEvent($this->getUser(), ['timesheet' => $timesheet], 'timesheet', $view);
|
||||
@@ -93,8 +90,6 @@ final class ActionsController extends BaseApiController
|
||||
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
|
||||
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
|
||||
#[Route(methods: ['GET'], path: '/activity/{id}/{view}/{locale}', name: 'get_activity_actions', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function getActivityActions(Activity $activity, string $view, string $locale): Response
|
||||
{
|
||||
$event = new PageActionsEvent($this->getUser(), ['activity' => $activity], 'activity', $view);
|
||||
@@ -113,8 +108,6 @@ final class ActionsController extends BaseApiController
|
||||
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
|
||||
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
|
||||
#[Route(methods: ['GET'], path: '/project/{id}/{view}/{locale}', name: 'get_project_actions', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function getProjectActions(Project $project, string $view, string $locale): Response
|
||||
{
|
||||
$event = new PageActionsEvent($this->getUser(), ['project' => $project], 'project', $view);
|
||||
@@ -133,8 +126,6 @@ final class ActionsController extends BaseApiController
|
||||
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
|
||||
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
|
||||
#[Route(methods: ['GET'], path: '/customer/{id}/{view}/{locale}', name: 'get_customer_actions', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function getCustomerActions(Customer $customer, string $view, string $locale): Response
|
||||
{
|
||||
$event = new PageActionsEvent($this->getUser(), ['customer' => $customer], 'customer', $view);
|
||||
|
||||
@@ -24,7 +24,6 @@ use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
@@ -34,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/activities')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('API')]
|
||||
#[OA\Tag(name: 'Activity')]
|
||||
final class ActivityController extends BaseApiController
|
||||
{
|
||||
@@ -44,10 +43,10 @@ final class ActivityController extends BaseApiController
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private ActivityRepository $repository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private ActivityRateRepository $activityRateRepository
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly ActivityRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ActivityRateRepository $activityRateRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -56,8 +55,6 @@ final class ActivityController extends BaseApiController
|
||||
*/
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of activities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityCollection')))]
|
||||
#[Route(methods: ['GET'], path: '', name: 'get_activities')]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\QueryParam(name: 'project', requirements: '\d+', strict: true, nullable: true, description: 'Project ID to filter activities')]
|
||||
#[Rest\QueryParam(name: 'projects', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of project IDs to filter activities, e.g.: projects[]=1&projects[]=2')]
|
||||
#[Rest\QueryParam(name: 'visible', requirements: '1|2|3', default: 1, strict: true, nullable: true, description: 'Visibility status to filter activities: 1=visible, 2=hidden, 3=all')]
|
||||
@@ -65,13 +62,13 @@ final class ActivityController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name|project', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name, project (default: name)')]
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, ProjectRepository $projectRepository): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$query->loadTeams();
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
@@ -112,9 +109,9 @@ final class ActivityController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getActivitiesForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
@@ -126,8 +123,7 @@ final class ActivityController extends BaseApiController
|
||||
#[OA\Response(response: 200, description: 'Returns one activity entity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))]
|
||||
#[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to fetch', required: true)]
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_activity', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[IsGranted('view', 'activity')]
|
||||
public function getAction(Activity $activity): Response
|
||||
{
|
||||
$view = new View($activity, 200);
|
||||
@@ -142,8 +138,6 @@ final class ActivityController extends BaseApiController
|
||||
#[OA\Post(description: 'Creates a new activity and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created activity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))])]
|
||||
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityEditForm'))]
|
||||
#[Route(methods: ['POST'], path: '', name: 'post_activity')]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function postAction(Request $request): Response
|
||||
{
|
||||
if (!$this->isGranted('create_activity')) {
|
||||
@@ -185,8 +179,6 @@ final class ActivityController extends BaseApiController
|
||||
#[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)]
|
||||
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_activity', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function patchAction(Request $request, Activity $activity): Response
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
@@ -222,8 +214,6 @@ final class ActivityController extends BaseApiController
|
||||
#[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)]
|
||||
#[Route(methods: ['PATCH'], path: '/{id}/meta', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\RequestParam(name: 'name', strict: true, nullable: false, description: 'The meta-field name')]
|
||||
#[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')]
|
||||
public function metaAction(Activity $activity, ParamFetcherInterface $paramFetcher): Response
|
||||
@@ -255,8 +245,6 @@ final class ActivityController extends BaseApiController
|
||||
#[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)]
|
||||
#[Route(methods: ['GET'], path: '/{id}/rates', name: 'get_activity_rates', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function getRatesAction(Activity $activity): Response
|
||||
{
|
||||
$rates = $this->activityRateRepository->getRatesForActivity($activity);
|
||||
@@ -274,8 +262,6 @@ final class ActivityController extends BaseApiController
|
||||
#[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')]
|
||||
#[Route(methods: ['DELETE'], path: '/{id}/rates/{rateId}', name: 'delete_activity_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])]
|
||||
public function deleteRateAction(Activity $activity, #[MapEntity(mapping: ['rateId' => 'id'])] ActivityRate $rate): Response
|
||||
{
|
||||
@@ -298,8 +284,6 @@ final class ActivityController extends BaseApiController
|
||||
#[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'))]
|
||||
#[Route(methods: ['POST'], path: '/{id}/rates', name: 'post_activity_rate', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function postRateAction(Activity $activity, Request $request): Response
|
||||
{
|
||||
$rate = new ActivityRate();
|
||||
|
||||
46
src/API/Authentication/AccessTokenHandler.php
Normal file
46
src/API/Authentication/AccessTokenHandler.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API\Authentication;
|
||||
|
||||
use App\Repository\AccessTokenRepository;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
|
||||
final class AccessTokenHandler implements AccessTokenHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccessTokenRepository $accessTokenRepository
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUserBadgeFrom(string $accessToken): UserBadge
|
||||
{
|
||||
$accessToken = $this->accessTokenRepository->findByToken($accessToken);
|
||||
|
||||
if (null === $accessToken) {
|
||||
throw new BadCredentialsException('Invalid credentials.');
|
||||
}
|
||||
|
||||
if (!$accessToken->isValid()) {
|
||||
throw new BadCredentialsException('Invalid token.');
|
||||
}
|
||||
|
||||
$now = new \DateTimeImmutable();
|
||||
// record last usage only if this is the first time OR once every minute
|
||||
if ($accessToken->getLastUsage() === null || $now->getTimestamp() > $accessToken->getLastUsage()->getTimestamp() + 60) {
|
||||
$accessToken->setLastUsage($now);
|
||||
$this->accessTokenRepository->saveAccessToken($accessToken);
|
||||
}
|
||||
|
||||
return new UserBadge($accessToken->getUser()->getUserIdentifier(), fn (string $userIdentifier) => $accessToken->getUser());
|
||||
}
|
||||
}
|
||||
25
src/API/Authentication/AccessTokenSuccessHandler.php
Normal file
25
src/API/Authentication/AccessTokenSuccessHandler.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API\Authentication;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
|
||||
final class AccessTokenSuccessHandler implements AuthenticationSuccessHandlerInterface
|
||||
{
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
|
||||
{
|
||||
$token->setAttribute('api-token', true);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,35 @@ final class ApiRequestMatcher implements RequestMatcherInterface
|
||||
{
|
||||
public function matches(Request $request): bool
|
||||
{
|
||||
if (str_contains($request->getRequestUri(), '/api/doc')) {
|
||||
// we do not want to handle URLs that are not in the API scope
|
||||
if (!str_starts_with($request->getRequestUri(), '/api/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||
// API documentation is only available to registered and logged-in users
|
||||
if (str_starts_with($request->getRequestUri(), '/api/doc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !$request->headers->has(SessionAuthenticator::HEADER_JAVASCRIPT) &&
|
||||
$request->headers->has(TokenAuthenticator::HEADER_USERNAME) &&
|
||||
$request->headers->has(TokenAuthenticator::HEADER_TOKEN);
|
||||
// ------------------------------------------------------------------------------------
|
||||
// the next two checks are primarily here to make sure to return proper error messages
|
||||
|
||||
// let's use this firewall if a Bearer token is set in the header
|
||||
// other cases like "bearer" are rejected earlier
|
||||
if (($auth = $request->headers->get('Authorization')) !== null && str_starts_with($auth, 'Bearer ')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// let's use this firewall if the deprecated username & token combination is available
|
||||
if ($request->headers->has(TokenAuthenticator::HEADER_USERNAME) &&
|
||||
$request->headers->has(TokenAuthenticator::HEADER_TOKEN)) {
|
||||
return true;
|
||||
}
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
// checking for a previous session allows us to skip the API firewall and token access handler
|
||||
// we simply re-use the existing session when doing API calls from the frontend.
|
||||
// it is not necessary to check headers. if there is no valid session, we should always use this firewall
|
||||
return !$request->hasPreviousSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API\Authentication;
|
||||
|
||||
use Scheb\TwoFactorBundle\Security\Http\Authenticator\TwoFactorAuthenticator;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
|
||||
final class SessionAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public const HEADER_JAVASCRIPT = 'X-AUTH-SESSION';
|
||||
|
||||
public function __construct(private TokenAuthenticator $authenticator)
|
||||
{
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||
// API docs can only be access, when the user is logged in
|
||||
if (str_contains($request->getRequestUri(), '/api/doc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !$request->headers->has(self::HEADER_JAVASCRIPT);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
return $this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
return $this->authenticator->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,25 @@ final class TokenAuthenticator extends AbstractAuthenticator
|
||||
public const HEADER_USERNAME = 'X-AUTH-USER';
|
||||
public const HEADER_TOKEN = 'X-AUTH-TOKEN';
|
||||
|
||||
public function __construct(private ApiUserRepository $userProvider, private PasswordHasherFactoryInterface $passwordHasherFactory)
|
||||
public function __construct(
|
||||
private readonly ApiUserRepository $userProvider,
|
||||
private readonly PasswordHasherFactoryInterface $passwordHasherFactory
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||
return !str_contains($request->getRequestUri(), '/api/doc');
|
||||
if (str_contains($request->getRequestUri(), '/api/doc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->headers->has(self::HEADER_USERNAME) && $request->headers->has(self::HEADER_TOKEN)) {
|
||||
@trigger_error('You are using deprecated API access, please upgrade your APP to use API tokens instead.', E_USER_DEPRECATED);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
@@ -52,6 +53,46 @@ abstract class BaseApiController extends AbstractController
|
||||
return DateTimeFactory::createByUser($user);
|
||||
}
|
||||
|
||||
protected function prepareQuery(BaseQuery $query, ParamFetcherInterface $paramFetcher): void
|
||||
{
|
||||
$query->setIsApiCall(true);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
// there is no function has() in ParamFetcherInterface, so we need to use all() and check for the key
|
||||
$all = $paramFetcher->all(true);
|
||||
|
||||
if (\array_key_exists('page', $all)) {
|
||||
$page = $all['page'];
|
||||
if (is_numeric($page)) {
|
||||
$query->setPage((int) $page);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('size', $all)) {
|
||||
$size = $all['size'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('pageSize', $all)) {
|
||||
$size = $all['pageSize'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createPaginatedView(Pagination $pagination): View
|
||||
{
|
||||
$results = (array) $pagination->getCurrentPageResults();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $pagination);
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
protected function addPagination(View $view, Pagination $pagination): void
|
||||
{
|
||||
$view->setHeader('X-Page', (string) $pagination->getCurrentPage());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user