From bf831dfa9d37d9d839631c09e611456be00d5490 Mon Sep 17 00:00:00 2001 From: FlorianEisenmenger Date: Sun, 24 May 2026 01:07:12 +0200 Subject: [PATCH] initial checkin with maaaaannnny functions --- .gitignore | 1 + .../addon-metadata/phpmyadmin/manifest.yaml | 10 + httpdocs/.ddev/commands/host/phpmyadmin | 19 + httpdocs/.ddev/config.yaml | 296 + httpdocs/.ddev/docker-compose.phpmyadmin.yaml | 27 + .../docker-compose.phpmyadmin_norouter.yaml | 4 + .../.ddev/traefik/certs/testtimetracking.crt | 32 + .../.ddev/traefik/certs/testtimetracking.key | 29 + httpdocs/.editorconfig | 17 + httpdocs/.env | 48 + httpdocs/.env.dev | 4 + httpdocs/.gitignore | 17 + httpdocs/1-reset-and-seed.sh | 35 + httpdocs/LICENSE | 19 + httpdocs/README.md | 37 + httpdocs/assets/app.js | 11 + httpdocs/assets/scripts/calendar.js | 276 + httpdocs/assets/scripts/crud.js | 473 ++ httpdocs/assets/scripts/duration.js | 96 + httpdocs/assets/scripts/entries.js | 441 ++ httpdocs/assets/scripts/registration.js | 87 + httpdocs/assets/scripts/team.js | 223 + httpdocs/assets/styles/atoms/_buttons.scss | 67 + httpdocs/assets/styles/atoms/_inputs.scss | 89 + httpdocs/assets/styles/atoms/_typography.scss | 37 + httpdocs/assets/styles/atoms/_variables.scss | 75 + .../assets/styles/components/_account.scss | 156 + httpdocs/assets/styles/components/_crud.scss | 208 + .../styles/components/_duration-help.scss | 56 + .../assets/styles/components/_entry-form.scss | 50 + .../assets/styles/components/_entry-list.scss | 170 + .../assets/styles/components/_greeting.scss | 15 + httpdocs/assets/styles/components/_login.scss | 98 + .../assets/styles/components/_main-nav.scss | 50 + .../styles/components/_month-calendar.scss | 137 + .../assets/styles/components/_register.scss | 193 + httpdocs/assets/styles/components/_team.scss | 164 + .../assets/styles/components/_week-nav.scss | 122 + httpdocs/assets/styles/main.scss | 42 + httpdocs/assets/styles/sections/_home.scss | 67 + .../assets/styles/sections/_timetracking.scss | 54 + httpdocs/bin/console | 21 + httpdocs/composer.json | 84 + httpdocs/composer.lock | 6634 +++++++++++++++++ httpdocs/config/bundles.php | 12 + httpdocs/config/packages/cache.yaml | 19 + httpdocs/config/packages/csrf.yaml | 11 + httpdocs/config/packages/doctrine.yaml | 76 + .../config/packages/doctrine_migrations.yaml | 5 + httpdocs/config/packages/framework.yaml | 19 + httpdocs/config/packages/mailer.yaml | 5 + httpdocs/config/packages/monolog.yaml | 20 + httpdocs/config/packages/property_info.yaml | 3 + httpdocs/config/packages/routing.yaml | 10 + httpdocs/config/packages/security.yaml | 65 + httpdocs/config/packages/translation.yaml | 7 + httpdocs/config/packages/twig.yaml | 6 + httpdocs/config/packages/validator.yaml | 11 + httpdocs/config/packages/webpack_encore.yaml | 45 + httpdocs/config/preload.php | 5 + httpdocs/config/reference.php | 1550 ++++ httpdocs/config/routes.yaml | 11 + httpdocs/config/routes/framework.yaml | 4 + httpdocs/config/routes/security.yaml | 3 + httpdocs/config/services.yaml | 60 + httpdocs/migrations/.gitignore | 0 httpdocs/migrations/central/.gitkeep | 0 .../central/Version20260523122322.php | 39 + .../central/Version20260523190211.php | 31 + .../central/Version20260523203200.php | 31 + .../central/Version20260523212725.php | 33 + .../central/Version20260523221257.php | 31 + httpdocs/migrations/tenant/.gitkeep | 0 httpdocs/package-lock.json | 5812 +++++++++++++++ httpdocs/package.json | 21 + httpdocs/public/index.php | 9 + httpdocs/src/Command/SeedCommand.php | 198 + httpdocs/src/Controller/.gitignore | 0 httpdocs/src/Controller/AccountController.php | 125 + httpdocs/src/Controller/ClientController.php | 143 + httpdocs/src/Controller/HomeController.php | 26 + httpdocs/src/Controller/InviteController.php | 101 + httpdocs/src/Controller/ProjectController.php | 146 + .../src/Controller/RegistrationController.php | 103 + .../src/Controller/SecurityController.php | 38 + httpdocs/src/Controller/ServiceController.php | 138 + httpdocs/src/Controller/TeamController.php | 313 + .../src/Controller/TimeTrackingController.php | 265 + .../Doctrine/TenantConnectionMiddleware.php | 38 + httpdocs/src/Entity/.gitignore | 0 httpdocs/src/Entity/Central/Account.php | 70 + httpdocs/src/Entity/Central/AccountUser.php | 65 + httpdocs/src/Entity/Central/InviteToken.php | 66 + .../src/Entity/Central/RegistrationToken.php | 69 + httpdocs/src/Entity/Central/User.php | 59 + httpdocs/src/Entity/Tenant/Client.php | 71 + httpdocs/src/Entity/Tenant/Project.php | 47 + httpdocs/src/Entity/Tenant/Service.php | 46 + httpdocs/src/Entity/Tenant/TimeEntry.php | 102 + .../SlidingSessionSubscriber.php | 57 + .../TenantRequestSubscriber.php | 56 + httpdocs/src/Kernel.php | 11 + httpdocs/src/Repository/.gitignore | 0 .../Repository/Central/AccountRepository.php | 15 + .../Central/AccountUserRepository.php | 10 + .../Central/InviteTokenRepository.php | 15 + .../Central/RegistrationTokenRepository.php | 25 + .../src/Repository/Central/UserRepository.php | 15 + .../Repository/Tenant/ClientRepository.php | 32 + .../Repository/Tenant/ProjectRepository.php | 39 + .../Repository/Tenant/ServiceRepository.php | 34 + .../Repository/Tenant/TimeEntryRepository.php | 89 + httpdocs/src/Security/AccessDeniedHandler.php | 22 + httpdocs/src/Service/AccountRoleHelper.php | 40 + httpdocs/src/Service/RegistrationService.php | 192 + httpdocs/src/Service/SlugGenerator.php | 165 + httpdocs/src/Service/TenantContext.php | 29 + httpdocs/src/Twig/AppExtension.php | 77 + .../src/Twig/Runtime/AppExtensionRuntime.php | 18 + httpdocs/symfony.lock | 220 + httpdocs/templates/_nav.html.twig | 41 + httpdocs/templates/account/index.html.twig | 219 + httpdocs/templates/base.html.twig | 26 + httpdocs/templates/client/index.html.twig | 133 + .../email/registration_confirm.html.twig | 35 + .../email/registration_notify.html.twig | 15 + .../email/registration_welcome.html.twig | 30 + .../templates/email/team_invite.html.twig | 32 + httpdocs/templates/home/index.html.twig | 28 + httpdocs/templates/invite/error.html.twig | 16 + .../templates/invite/set_password.html.twig | 58 + httpdocs/templates/project/index.html.twig | 139 + .../registration/confirm_error.html.twig | 26 + .../registration/confirmed.html.twig | 30 + .../templates/registration/register.html.twig | 96 + httpdocs/templates/security/login.html.twig | 65 + httpdocs/templates/service/index.html.twig | 141 + httpdocs/templates/team/index.html.twig | 237 + .../timetracking/_entry_row.html.twig | 81 + .../templates/timetracking/week.html.twig | 237 + httpdocs/translations/.gitignore | 0 httpdocs/translations/messages.de.yaml | 43 + httpdocs/webpack.config.js | 79 + 143 files changed, 23942 insertions(+) create mode 100644 .gitignore create mode 100644 httpdocs/.ddev/addon-metadata/phpmyadmin/manifest.yaml create mode 100755 httpdocs/.ddev/commands/host/phpmyadmin create mode 100644 httpdocs/.ddev/config.yaml create mode 100644 httpdocs/.ddev/docker-compose.phpmyadmin.yaml create mode 100644 httpdocs/.ddev/docker-compose.phpmyadmin_norouter.yaml create mode 100644 httpdocs/.ddev/traefik/certs/testtimetracking.crt create mode 100644 httpdocs/.ddev/traefik/certs/testtimetracking.key create mode 100644 httpdocs/.editorconfig create mode 100644 httpdocs/.env create mode 100644 httpdocs/.env.dev create mode 100644 httpdocs/.gitignore create mode 100644 httpdocs/1-reset-and-seed.sh create mode 100644 httpdocs/LICENSE create mode 100644 httpdocs/README.md create mode 100644 httpdocs/assets/app.js create mode 100644 httpdocs/assets/scripts/calendar.js create mode 100644 httpdocs/assets/scripts/crud.js create mode 100644 httpdocs/assets/scripts/duration.js create mode 100644 httpdocs/assets/scripts/entries.js create mode 100644 httpdocs/assets/scripts/registration.js create mode 100644 httpdocs/assets/scripts/team.js create mode 100644 httpdocs/assets/styles/atoms/_buttons.scss create mode 100644 httpdocs/assets/styles/atoms/_inputs.scss create mode 100644 httpdocs/assets/styles/atoms/_typography.scss create mode 100644 httpdocs/assets/styles/atoms/_variables.scss create mode 100644 httpdocs/assets/styles/components/_account.scss create mode 100644 httpdocs/assets/styles/components/_crud.scss create mode 100644 httpdocs/assets/styles/components/_duration-help.scss create mode 100644 httpdocs/assets/styles/components/_entry-form.scss create mode 100644 httpdocs/assets/styles/components/_entry-list.scss create mode 100644 httpdocs/assets/styles/components/_greeting.scss create mode 100644 httpdocs/assets/styles/components/_login.scss create mode 100644 httpdocs/assets/styles/components/_main-nav.scss create mode 100644 httpdocs/assets/styles/components/_month-calendar.scss create mode 100644 httpdocs/assets/styles/components/_register.scss create mode 100644 httpdocs/assets/styles/components/_team.scss create mode 100644 httpdocs/assets/styles/components/_week-nav.scss create mode 100644 httpdocs/assets/styles/main.scss create mode 100644 httpdocs/assets/styles/sections/_home.scss create mode 100644 httpdocs/assets/styles/sections/_timetracking.scss create mode 100755 httpdocs/bin/console create mode 100644 httpdocs/composer.json create mode 100644 httpdocs/composer.lock create mode 100644 httpdocs/config/bundles.php create mode 100644 httpdocs/config/packages/cache.yaml create mode 100644 httpdocs/config/packages/csrf.yaml create mode 100644 httpdocs/config/packages/doctrine.yaml create mode 100644 httpdocs/config/packages/doctrine_migrations.yaml create mode 100644 httpdocs/config/packages/framework.yaml create mode 100644 httpdocs/config/packages/mailer.yaml create mode 100644 httpdocs/config/packages/monolog.yaml create mode 100644 httpdocs/config/packages/property_info.yaml create mode 100644 httpdocs/config/packages/routing.yaml create mode 100644 httpdocs/config/packages/security.yaml create mode 100644 httpdocs/config/packages/translation.yaml create mode 100644 httpdocs/config/packages/twig.yaml create mode 100644 httpdocs/config/packages/validator.yaml create mode 100644 httpdocs/config/packages/webpack_encore.yaml create mode 100644 httpdocs/config/preload.php create mode 100644 httpdocs/config/reference.php create mode 100644 httpdocs/config/routes.yaml create mode 100644 httpdocs/config/routes/framework.yaml create mode 100644 httpdocs/config/routes/security.yaml create mode 100644 httpdocs/config/services.yaml create mode 100644 httpdocs/migrations/.gitignore create mode 100644 httpdocs/migrations/central/.gitkeep create mode 100644 httpdocs/migrations/central/Version20260523122322.php create mode 100644 httpdocs/migrations/central/Version20260523190211.php create mode 100644 httpdocs/migrations/central/Version20260523203200.php create mode 100644 httpdocs/migrations/central/Version20260523212725.php create mode 100644 httpdocs/migrations/central/Version20260523221257.php create mode 100644 httpdocs/migrations/tenant/.gitkeep create mode 100644 httpdocs/package-lock.json create mode 100644 httpdocs/package.json create mode 100644 httpdocs/public/index.php create mode 100644 httpdocs/src/Command/SeedCommand.php create mode 100644 httpdocs/src/Controller/.gitignore create mode 100644 httpdocs/src/Controller/AccountController.php create mode 100644 httpdocs/src/Controller/ClientController.php create mode 100644 httpdocs/src/Controller/HomeController.php create mode 100644 httpdocs/src/Controller/InviteController.php create mode 100644 httpdocs/src/Controller/ProjectController.php create mode 100644 httpdocs/src/Controller/RegistrationController.php create mode 100644 httpdocs/src/Controller/SecurityController.php create mode 100644 httpdocs/src/Controller/ServiceController.php create mode 100644 httpdocs/src/Controller/TeamController.php create mode 100644 httpdocs/src/Controller/TimeTrackingController.php create mode 100644 httpdocs/src/Doctrine/TenantConnectionMiddleware.php create mode 100644 httpdocs/src/Entity/.gitignore create mode 100644 httpdocs/src/Entity/Central/Account.php create mode 100644 httpdocs/src/Entity/Central/AccountUser.php create mode 100644 httpdocs/src/Entity/Central/InviteToken.php create mode 100644 httpdocs/src/Entity/Central/RegistrationToken.php create mode 100644 httpdocs/src/Entity/Central/User.php create mode 100644 httpdocs/src/Entity/Tenant/Client.php create mode 100644 httpdocs/src/Entity/Tenant/Project.php create mode 100644 httpdocs/src/Entity/Tenant/Service.php create mode 100644 httpdocs/src/Entity/Tenant/TimeEntry.php create mode 100644 httpdocs/src/EventSubscriber/SlidingSessionSubscriber.php create mode 100644 httpdocs/src/EventSubscriber/TenantRequestSubscriber.php create mode 100644 httpdocs/src/Kernel.php create mode 100644 httpdocs/src/Repository/.gitignore create mode 100644 httpdocs/src/Repository/Central/AccountRepository.php create mode 100644 httpdocs/src/Repository/Central/AccountUserRepository.php create mode 100644 httpdocs/src/Repository/Central/InviteTokenRepository.php create mode 100644 httpdocs/src/Repository/Central/RegistrationTokenRepository.php create mode 100644 httpdocs/src/Repository/Central/UserRepository.php create mode 100644 httpdocs/src/Repository/Tenant/ClientRepository.php create mode 100644 httpdocs/src/Repository/Tenant/ProjectRepository.php create mode 100644 httpdocs/src/Repository/Tenant/ServiceRepository.php create mode 100644 httpdocs/src/Repository/Tenant/TimeEntryRepository.php create mode 100644 httpdocs/src/Security/AccessDeniedHandler.php create mode 100644 httpdocs/src/Service/AccountRoleHelper.php create mode 100644 httpdocs/src/Service/RegistrationService.php create mode 100644 httpdocs/src/Service/SlugGenerator.php create mode 100644 httpdocs/src/Service/TenantContext.php create mode 100644 httpdocs/src/Twig/AppExtension.php create mode 100644 httpdocs/src/Twig/Runtime/AppExtensionRuntime.php create mode 100644 httpdocs/symfony.lock create mode 100644 httpdocs/templates/_nav.html.twig create mode 100644 httpdocs/templates/account/index.html.twig create mode 100644 httpdocs/templates/base.html.twig create mode 100644 httpdocs/templates/client/index.html.twig create mode 100644 httpdocs/templates/email/registration_confirm.html.twig create mode 100644 httpdocs/templates/email/registration_notify.html.twig create mode 100644 httpdocs/templates/email/registration_welcome.html.twig create mode 100644 httpdocs/templates/email/team_invite.html.twig create mode 100644 httpdocs/templates/home/index.html.twig create mode 100644 httpdocs/templates/invite/error.html.twig create mode 100644 httpdocs/templates/invite/set_password.html.twig create mode 100644 httpdocs/templates/project/index.html.twig create mode 100644 httpdocs/templates/registration/confirm_error.html.twig create mode 100644 httpdocs/templates/registration/confirmed.html.twig create mode 100644 httpdocs/templates/registration/register.html.twig create mode 100644 httpdocs/templates/security/login.html.twig create mode 100644 httpdocs/templates/service/index.html.twig create mode 100644 httpdocs/templates/team/index.html.twig create mode 100644 httpdocs/templates/timetracking/_entry_row.html.twig create mode 100644 httpdocs/templates/timetracking/week.html.twig create mode 100644 httpdocs/translations/.gitignore create mode 100644 httpdocs/translations/messages.de.yaml create mode 100644 httpdocs/webpack.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/httpdocs/.ddev/addon-metadata/phpmyadmin/manifest.yaml b/httpdocs/.ddev/addon-metadata/phpmyadmin/manifest.yaml new file mode 100644 index 0000000..d964bf9 --- /dev/null +++ b/httpdocs/.ddev/addon-metadata/phpmyadmin/manifest.yaml @@ -0,0 +1,10 @@ +name: phpmyadmin +repository: ddev/ddev-phpmyadmin +version: v1.0.3 +install_date: "2026-05-22T08:18:15+02:00" +project_files: + - docker-compose.phpmyadmin.yaml + - docker-compose.phpmyadmin_norouter.yaml + - commands/host/phpmyadmin +global_files: [] +removal_actions: [] diff --git a/httpdocs/.ddev/commands/host/phpmyadmin b/httpdocs/.ddev/commands/host/phpmyadmin new file mode 100755 index 0000000..5f7d0ed --- /dev/null +++ b/httpdocs/.ddev/commands/host/phpmyadmin @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +## #ddev-generated: If you want to edit and own this file, remove this line. +## Description: Launch a browser with PhpMyAdmin +## Usage: phpmyadmin +## Example: "ddev phpmyadmin" + +DDEV_PHPMYADMIN_PORT=8036 +DDEV_PHPMYADMIN_HTTPS_PORT=8037 +if [ ${DDEV_PRIMARY_URL%://*} = "http" ] || [ -n "${GITPOD_WORKSPACE_ID:-}" ] || [ "${CODESPACES:-}" = "true" ]; then + # Gitpod: "gp preview" opens a blank page for PhpMyAdmin, use "xdg-open" instead + if [ "${OSTYPE:-}" = "linux-gnu" ] && [ -n "${GITPOD_WORKSPACE_ID:-}" ] && [ -z "${DDEV_DEBUG:-}" ]; then + xdg-open "$(DDEV_DEBUG=true ddev launch :$DDEV_PHPMYADMIN_PORT | grep "FULLURL" | awk '{print $2}')" + else + ddev launch :$DDEV_PHPMYADMIN_PORT + fi +else + ddev launch :$DDEV_PHPMYADMIN_HTTPS_PORT +fi diff --git a/httpdocs/.ddev/config.yaml b/httpdocs/.ddev/config.yaml new file mode 100644 index 0000000..27da251 --- /dev/null +++ b/httpdocs/.ddev/config.yaml @@ -0,0 +1,296 @@ +name: timetracking +type: php +docroot: public +php_version: "8.4" +webserver_type: nginx-fpm +xdebug_enabled: false +additional_hostnames: + [] +additional_fqdns: + - spawntree.timetracking.ddev.site + - nova-sign.timetracking.ddev.site +database: + type: mariadb + version: "10.11" +use_dns_when_possible: true +composer_version: "2" +web_environment: [] +corepack_enable: false +router_http_port: "8089" +router_https_port: "8459" + +# Key features of DDEV's config.yaml: + +# name: # Name of the project, automatically provides +# http://projectname.ddev.site and https://projectname.ddev.site +# If the name is omitted, the project will take the name of the enclosing directory, +# which is useful if you want to have a copy of the project side by side with this one. + +# type: # asterios, backdrop, cakephp, codeigniter, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, drupal12, generic, joomla, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress, wp-bedrock +# See https://docs.ddev.com/en/stable/users/quickstart/ for more +# information on the different project types + +# docroot: # Relative path to the directory containing index.php. + +# php_version: "8.4" # PHP version to use, "5.6" through "8.5" + +# You can explicitly specify the webimage but this +# is not recommended, as the images are often closely tied to DDEV's' behavior, +# so this can break upgrades. + +# webimage: +# It’s unusual to change this option, and we don’t recommend it without Docker experience and a good reason. +# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.* + +# database: +# type: # mysql, mariadb, postgres +# version: # database version, like "10.11" or "8.0" +# MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8 +# MySQL versions can be 5.5-8.0, 8.4 +# PostgreSQL versions can be 9-18 + +# router_http_port: # Port to be used for http (defaults to global configuration, usually 80) +# router_https_port: # Port for https (defaults to global configuration, usually 443) + +# xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart" +# Note that for most people the commands +# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, +# as leaving Xdebug enabled all the time is a big performance hit. + +# xhgui_http_port: "8143" +# xhgui_https_port: "8142" +# The XHGui ports can be changed from the default 8143 and 8142 +# Very rarely used + +# host_xhgui_port: "8142" +# Can be used to change the host binding port of the XHGui +# application. Rarely used; only when port conflict and +# bind_all_ports is used (normally with router disabled) + +# xhprof_mode: [prepend|xhgui|global] +# Default is "xhgui" + +# webserver_type: nginx-fpm, apache-fpm, generic + +# timezone: Europe/Berlin +# If timezone is unset, DDEV will attempt to derive it from the host system timezone +# using the $TZ environment variable or the /etc/localtime symlink. +# This is the timezone used in the containers and by PHP; +# it can be set to any valid timezone, +# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +# For example Europe/Dublin or MST7MDT + +# composer_root: +# Relative path to the Composer root directory from the project root. This is +# the directory which contains the composer.json and where all Composer related +# commands are executed. + +# composer_version: "2" +# You can set it to "" or "2" (default) for Composer v2 +# to use the latest major version available at the time your container is built. +# It is also possible to use each other Composer version channel. This includes: +# - 2.2 (latest Composer LTS version) +# - stable +# - preview +# - snapshot +# Alternatively, an explicit Composer version may be specified, for example "2.2.18". +# To reinstall Composer after the image was built, run "ddev utility rebuild". + +# nodejs_version: "24" +# change from the default system Node.js version to any other version. +# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information +# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation. + +# corepack_enable: false +# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm + +# additional_hostnames: +# - somename +# - someothername +# would provide http and https URLs for "somename.ddev.site" +# and "someothername.ddev.site". + +# additional_fqdns: +# - example.com +# - sub1.example.com +# would provide http and https URLs for "example.com" and "sub1.example.com" +# Please take care with this because it can cause great confusion. + +# upload_dirs: "custom/upload/dir" +# +# upload_dirs: +# - custom/upload/dir +# - ../private +# +# would set the destination paths for ddev import-files to /custom/upload/dir +# When Mutagen is enabled this path is bind-mounted so that all the files +# in the upload_dirs don't have to be synced into Mutagen. + +# disable_upload_dirs_warning: false +# If true, turns off the normal warning that says +# "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set" + +# ddev_version_constraint: "" +# Example: +# ddev_version_constraint: ">= 1.24.8" +# This will enforce that the running ddev version is within this constraint. +# See https://github.com/Masterminds/semver#checking-version-constraints for +# supported constraint formats + +# working_dir: +# web: /var/www/html +# db: /home +# would set the default working directory for the web and db services. +# These values specify the destination directory for ddev ssh and the +# directory in which commands passed into ddev exec are run. + +# omit_containers: [db, ddev-ssh-agent] +# Currently only these containers are supported. Some containers can also be +# omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit +# the "db" container, several standard features of DDEV that access the +# database container will be unusable. In the global configuration it is also +# possible to omit ddev-router, but not here. + +# performance_mode: "global" +# DDEV offers performance optimization strategies to improve the filesystem +# performance depending on your host system. Should be configured globally. +# +# If set, will override the global config. Possible values are: +# - "global": uses the value from the global config. +# - "none": disables performance optimization for this project. +# - "mutagen": enables Mutagen for this project. +# +# See https://docs.ddev.com/en/stable/users/install/performance/#mutagen + +# fail_on_hook_fail: False +# Decide whether 'ddev start' should be interrupted by a failing hook + +# host_https_port: "59002" +# The host port binding for https can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_webserver_port: "59001" +# The host port binding for the ddev-webserver can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_db_port: "59002" +# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic +# unless explicitly specified. + +# mailpit_http_port: "8025" +# mailpit_https_port: "8026" +# The Mailpit ports can be changed from the default 8025 and 8026 + +# host_mailpit_port: "8025" +# The mailpit port is not normally bound on the host at all, instead being routed +# through ddev-router, but it can be bound directly to localhost if specified here. + +# webimage_extra_packages: ['php${DDEV_PHP_VERSION}-tidy', 'php${DDEV_PHP_VERSION}-yac'] +# Extra Debian packages that are needed in the webimage can be added here + +# dbimage_extra_packages: [netcat, telnet, sudo] +# Extra Debian packages that are needed in the dbimage can be added here + +# use_dns_when_possible: true +# If the host has internet access and the domain configured can +# successfully be looked up, DNS will be used for hostname resolution +# instead of editing /etc/hosts +# Defaults to true + +# project_tld: ddev.site +# The top-level domain used for project URLs +# The default "ddev.site" allows DNS lookup via a wildcard + +# share_default_provider: ngrok +# The default share provider to use for "ddev share" +# Defaults to global configuration, usually "ngrok" +# Can be "ngrok" or "cloudflared" or the name of a custom provider from .ddev/share-providers/ + +# share_provider_args: --basic-auth username:pass1234 +# Provide extra flags to the share provider script +# See https://docs.ddev.com/en/stable/users/configuration/config/#share_provider_args + +# disable_settings_management: false +# If true, DDEV will not create CMS-specific settings files like +# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php +# In this case the user must provide all such settings. + +# You can inject environment variables into the web container with: +# web_environment: +# - SOMEENV=somevalue +# - SOMEOTHERENV=someothervalue + +# no_project_mount: false +# (Experimental) If true, DDEV will not mount the project into the web container; +# the user is responsible for mounting it manually or via a script. +# This is to enable experimentation with alternate file mounting strategies. +# For advanced users only! + +# bind_all_interfaces: false +# If true, host ports will be bound on all network interfaces, +# not the localhost interface only. This means that ports +# will be available on the local network if the host firewall +# allows it. + +# default_container_timeout: 120 +# The default time that DDEV waits for all containers to become ready can be increased from +# the default 120. This helps in importing huge databases, for example. + +#web_extra_exposed_ports: +#- name: nodejs +# container_port: 3000 +# http_port: 2999 +# https_port: 3000 +#- name: something +# container_port: 4000 +# https_port: 4000 +# http_port: 3999 +# Allows a set of extra ports to be exposed via ddev-router +# Fill in all three fields even if you don’t intend to use the https_port! +# If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start. +# +# The port behavior on the ddev-webserver must be arranged separately, for example +# using web_extra_daemons. +# For example, with a web app on port 3000 inside the container, this config would +# expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 +# web_extra_exposed_ports: +# - name: myapp +# container_port: 3000 +# http_port: 9998 +# https_port: 9999 + +#web_extra_daemons: +#- name: "http-1" +# command: "/var/www/html/node_modules/.bin/http-server -p 3000" +# directory: /var/www/html +#- name: "http-2" +# command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000" +# directory: /var/www/html + +# override_config: false +# By default, config.*.yaml files are *merged* into the configuration +# But this means that some things can't be overridden +# For example, if you have 'use_dns_when_possible: true'' you can't override it with a merge +# and you can't erase existing hooks or all environment variables. +# However, with "override_config: true" in a particular config.*.yaml file, +# 'use_dns_when_possible: false' can override the existing values, and +# hooks: +# post-start: [] +# or +# web_environment: [] +# or +# additional_hostnames: [] +# can have their intended affect. 'override_config' affects only behavior of the +# config.*.yaml file it exists in. + +# Many DDEV commands can be extended to run tasks before or after the +# DDEV command is executed, for example "post-start", "post-import-db", +# "pre-composer", "post-composer" +# See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more +# information on the commands that can be extended and the tasks you can define +# for them. Example: +#hooks: diff --git a/httpdocs/.ddev/docker-compose.phpmyadmin.yaml b/httpdocs/.ddev/docker-compose.phpmyadmin.yaml new file mode 100644 index 0000000..fae7ddc --- /dev/null +++ b/httpdocs/.ddev/docker-compose.phpmyadmin.yaml @@ -0,0 +1,27 @@ +#ddev-generated +services: + phpmyadmin: + container_name: ddev-${DDEV_SITENAME}-phpmyadmin + image: ${PHPMYADMIN_DOCKER_IMAGE:-phpmyadmin:5} + working_dir: "/root" + restart: "no" + labels: + com.ddev.site-name: ${DDEV_SITENAME} + com.ddev.approot: ${DDEV_APPROOT} + volumes: + - ".:/mnt/ddev_config" + - "ddev-global-cache:/mnt/ddev-global-cache" + expose: + - "80" + environment: + - PMA_USER=root + - PMA_PASSWORD=root + - PMA_HOST=db + - PMA_PORT=3306 + - VIRTUAL_HOST=$DDEV_HOSTNAME + - UPLOAD_LIMIT=4000M + - HTTP_EXPOSE=8036:80 + - HTTPS_EXPOSE=8037:80 + depends_on: + db: + condition: service_healthy diff --git a/httpdocs/.ddev/docker-compose.phpmyadmin_norouter.yaml b/httpdocs/.ddev/docker-compose.phpmyadmin_norouter.yaml new file mode 100644 index 0000000..f369b69 --- /dev/null +++ b/httpdocs/.ddev/docker-compose.phpmyadmin_norouter.yaml @@ -0,0 +1,4 @@ +#ddev-generated +# If omit_containers[ddev-router] then this file will be replaced +# with another with a `ports` statement to directly expose port 80 to 8036 +services: {} diff --git a/httpdocs/.ddev/traefik/certs/testtimetracking.crt b/httpdocs/.ddev/traefik/certs/testtimetracking.crt new file mode 100644 index 0000000..8fc2af9 --- /dev/null +++ b/httpdocs/.ddev/traefik/certs/testtimetracking.crt @@ -0,0 +1,32 @@ +#ddev-generated +-----BEGIN CERTIFICATE----- +MIIFUjCCA7qgAwIBAgIQJ/cWyC4WBkzi24Iit9v3gzANBgkqhkiG9w0BAQsFADCB +jTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTEwLwYDVQQLDChmbG9l +aXNATWFjQm9vay1Qcm8tdm9uLUZsb3JpYW4uZnJpdHouYm94MTgwNgYDVQQDDC9t +a2NlcnQgZmxvZWlzQE1hY0Jvb2stUHJvLXZvbi1GbG9yaWFuLmZyaXR6LmJveDAe +Fw0yNjA1MjMxOTM0NDhaFw0yODA4MjMxOTM0NDhaMGoxJzAlBgNVBAoTHm1rY2Vy +dCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTE/MD0GA1UECww2ZmxvZWlzQE1CUC12 +b24tRmxvcmlhbi5mcml0ei5ib3ggKEZsb3JpYW4gRWlzZW5tZW5nZXIpMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAze00g0lF6cZaFnmJvT0q6cmhqDW/ +7n9SQlVvUuRAIbQG0s74776P6t6q1/MaiivSdX7hZLFMi2iVkMKIuLtdUV7SXlNJ +1EZqRpNrwvUpoohfgEfDctZFNPSDmT3VUZir+axGEULjXw9ALAmKtAzo4riuTy6M +/1udXr0PZLxJ1wsDHbDWnkLwl9Y1fwv/tRANJ01xvHbdwCxLj2VBNngcUlI65P9Y +0ggGOGaXoI5MMt6gBnBbDUydJCRkvRLi5hooTbz56OYwhhGXDgQWOyXr6YBnto6+ +mZdnTgdMcpCidNS0IY6j5xs+68IFd8hGuf3XrhdjxhR/nyuBaDwhGN/mGwIDAQAB +o4IBTjCCAUowDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8G +A1UdIwQYMBaAFAV2EXvo8eNPfrl2DOI9m/YyNKwyMIIBAAYDVR0RBIH4MIH1ggsq +LmRkZXYuc2l0ZYIJbG9jYWxob3N0ggwqLmRkZXYubG9jYWyCC2RkZXYtcm91dGVy +ghBkZGV2LXJvdXRlci5kZGV2ghhkZGV2LXJvdXRlci5kZGV2X2RlZmF1bHSCGnRl +c3R0aW1ldHJhY2tpbmcuZGRldi5zaXRlgiRub3ZhLXNpZ24udGVzdHRpbWV0cmFj +a2luZy5kZGV2LnNpdGWCJnNwYXdudHJlZS0yLnRlc3R0aW1ldHJhY2tpbmcuZGRl +di5zaXRlgiRzcGF3bnRyZWUudGVzdHRpbWV0cmFja2luZy5kZGV2LnNpdGWHBH8A +AAEwDQYJKoZIhvcNAQELBQADggGBAGe6bsmZ/d8TKfjrUP65xxBZr73Y3OCg8is+ +exd8MCXlL9M29Wd2Jc4Qp0DRCqAsLcz8HCxN+XMc82WYNT/zJ3Av2jxqmodhtz4j +vqfWXBTrEdoKogEUmTrwAFbLxIS8RxvVqP/aiZDnuhbyGFaC8jWmOMdw4HWt3Bdj +1aut3fhoKUY1UyiP9C7UaDs82kiWCrRbH2iUTFh0HOWD5E/gFstxghqYUzXSQrWX +BTKjILUjIOrpfEULdY8NKNmgo/TKeyiQvQFi2x1ZYTgQVbCRCeC9/UF5Lx6tLQz5 +7pv0Ejj3GbITWCGtu0f5lSCDz7W8ESU2XTZg8Bu3oAp7T+A1Kd8hMgMjRsxoDpLS +qnKVXkzAO5NhOFrYK9U2NSydvuXAWOFQpynGyS/YGHNLlOQEiL8ENzeQ0RSSbRWd +259CmZIjICngaViNuOUwTkd2qmraXT4w5zy0qt7JaX7ocU+WSaFwbx+ftbkkRv5d +UH1NgWlduI255qogbBnpQql57EkwjQ== +-----END CERTIFICATE----- diff --git a/httpdocs/.ddev/traefik/certs/testtimetracking.key b/httpdocs/.ddev/traefik/certs/testtimetracking.key new file mode 100644 index 0000000..774c824 --- /dev/null +++ b/httpdocs/.ddev/traefik/certs/testtimetracking.key @@ -0,0 +1,29 @@ +#ddev-generated +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDN7TSDSUXpxloW +eYm9PSrpyaGoNb/uf1JCVW9S5EAhtAbSzvjvvo/q3qrX8xqKK9J1fuFksUyLaJWQ +woi4u11RXtJeU0nURmpGk2vC9SmiiF+AR8Ny1kU09IOZPdVRmKv5rEYRQuNfD0As +CYq0DOjiuK5PLoz/W51evQ9kvEnXCwMdsNaeQvCX1jV/C/+1EA0nTXG8dt3ALEuP +ZUE2eBxSUjrk/1jSCAY4Zpegjkwy3qAGcFsNTJ0kJGS9EuLmGihNvPno5jCGEZcO +BBY7JevpgGe2jr6Zl2dOB0xykKJ01LQhjqPnGz7rwgV3yEa5/deuF2PGFH+fK4Fo +PCEY3+YbAgMBAAECggEBAJDdhku1iFFlEIsvBQ7zsPS2u9qxtUv6lcvEfoQ5vkP3 +ebVjlQxTars32cgBZXI+UdgGYlmLwOVxtYYY1EXgyU0s/6ELxqxmvOzZWL3V6mxE +s6py0bQ/uIAAY3OyZBb66EDESKZr/7gn6mUQcVsomcylTzq07MvXj2XOar3bF7cG +7PwQM+Co+H/g4FmDy32IYACXSAWSQVQSlTGKb5AyXrdELUJ/keaHN9FwdbJQBEC3 +6yHaSSOJEQJvyqh+P1In8cJi4GQp7vDEnZEQTI+XH9H/9TjtUIL2FeOXreOEuvks +/jXgFEu/g/kyShrGEQT4+pYUTWGIiKP5LJa1xD+u+QECgYEA3l/WW6LlmVcykQEQ +7E/S4tJ1y1XCc8rHpvsHwqwhkEdWUuBDoWntlK2WzrEQZagibu9zV+BrHoa6MgUJ ++ElspLIdjjAOKMVu/hUcJoxKIMtRypibQRPGZZu7d6hytKC6aLN0ZtMwLUSF51wH +zSQzoVB5DZEF1FG/cVNRhe6x4ksCgYEA7RCrncJM/ML2tYlfwEacdnbUIydY6YBd +ngbXSpjlxHLp71r4/lBJig8kJVECX3PhIP/b8kNNJYfdtavbvbMot5FhTeuxf/EI +3Vl9YfrMbUs5/Wrf79yUO8pMFupDbdFkqMmqEA7J40YBmK7f0LkMn+p8EZaeOgwo +OnXUpUM3KXECgYBdIJmu6rtoymG85EtoC83ve+Ak9Zdn0sZmIb8QQfIUcCuwrYbl +NG1w1HnRucl6KT2yY8lURgHWWOnlRML2HhnHp2hFQc7MOFLRSZnuctYggcWRKPPr +/xIZP2z1IbBYAO/QJUdcQJlue8HwMFR8DusoZYEss01Tq6CXHyOHCX2pnQKBgQDi +w1x11mNQMKpPMi3OPXzy8G2xhrTM/sYOIFsV9zVp+cX9+BZPJbuCfUNFEr1jUvQZ +XcUlcu07pkAUxGS4i8S5+y2JnJe4W3bwTObbr0yWiyvYVcAJsAR3QOYR0VpYlMBl +mCm9nHfPl6p1Q2nCPRBvc5vkMx/9RJ3Cde3He4krcQKBgQDRN8NRUH64W5Oaw9Oo +K7S1JZCb3FqV4NxKKJrEWVyU6HTyCdLpcPd4gZtsmMHGr5M6/Uuq/FsZ0FVn2S6f +42wecSfmJ69BiFnVR/tRV+5sTg2RF/zrPnGKVld/mQHqwLiETyqg+c0Y1YAVagJy +qR3GtzV7LFRsUjkmmgsCiOQ2aQ== +-----END PRIVATE KEY----- diff --git a/httpdocs/.editorconfig b/httpdocs/.editorconfig new file mode 100644 index 0000000..6699076 --- /dev/null +++ b/httpdocs/.editorconfig @@ -0,0 +1,17 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[{compose.yaml,compose.*.yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/httpdocs/.env b/httpdocs/.env new file mode 100644 index 0000000..c8d21ca --- /dev/null +++ b/httpdocs/.env @@ -0,0 +1,48 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET= +APP_SHARE_DIR=var/share +###< symfony/framework-bundle ### + +###> symfony/routing ### +# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. +# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands +DEFAULT_URI=http://localhost +###< symfony/routing ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" +#DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" +###< doctrine/doctrine-bundle ### + +DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=10.11.2-MariaDB&charset=utf8mb4" +APP_DOMAIN=testtimetracking.ddev.site + +# ── Mailer ──────────────────────────────────────────────────────────────────── +# Lokal (DDEV Mailpit): smtp://127.0.0.1:1025 +# Produktion: smtp://user:pass@smtp.deinserver.de:587 +MAILER_DSN=smtp://127.0.0.1:1025 + +# Benachrichtigung bei Neuanmeldung +REGISTRATION_NOTIFY_EMAIL=re@spawntree.de diff --git a/httpdocs/.env.dev b/httpdocs/.env.dev new file mode 100644 index 0000000..18bbff6 --- /dev/null +++ b/httpdocs/.env.dev @@ -0,0 +1,4 @@ + +###> symfony/framework-bundle ### +APP_SECRET=f19f2bcb34a48e20e66302a0e88408a9 +###< symfony/framework-bundle ### diff --git a/httpdocs/.gitignore b/httpdocs/.gitignore new file mode 100644 index 0000000..7fdcc07 --- /dev/null +++ b/httpdocs/.gitignore @@ -0,0 +1,17 @@ + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> symfony/webpack-encore-bundle ### +/node_modules/ +/public/build/ +npm-debug.log +yarn-error.log +###< symfony/webpack-encore-bundle ### diff --git a/httpdocs/1-reset-and-seed.sh b/httpdocs/1-reset-and-seed.sh new file mode 100644 index 0000000..5780436 --- /dev/null +++ b/httpdocs/1-reset-and-seed.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +echo "⏳ Datenbank zurücksetzen..." +ddev exec php bin/console doctrine:database:drop --force --if-exists --connection=central +ddev exec php bin/console doctrine:database:create --connection=central +ddev mysql -uroot -proot -e "DROP DATABASE IF EXISTS db_spawntree;" +ddev mysql -uroot -proot -e "DROP DATABASE IF EXISTS db_nova_sign;" + +echo "⏳ Central-Migrationen ausführen..." +ddev exec php bin/console doctrine:migrations:migrate --em=central --no-interaction + +# db-User darf neue Tenant-DBs (db_*) anlegen und verwalten: +ddev mysql -uroot -proot -e "GRANT ALL PRIVILEGES ON \`db_%\`.* TO 'db'@'%'; FLUSH PRIVILEGES;" + +read -r -p "Testdaten einspielen? [j/N] " answer +if [[ "$answer" =~ ^[jJ]$ ]]; then + echo "⏳ Tenant-DBs anlegen..." + ddev mysql -uroot -proot -e "CREATE DATABASE db_spawntree CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + ddev mysql -uroot -proot -e "GRANT ALL PRIVILEGES ON db_spawntree.* TO 'db'@'%'; FLUSH PRIVILEGES;" + ddev mysql -uroot -proot -e "CREATE DATABASE db_nova_sign CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + ddev mysql -uroot -proot -e "GRANT ALL PRIVILEGES ON db_nova_sign.* TO 'db'@'%'; FLUSH PRIVILEGES;" + echo "⏳ Testdaten einspielen..." + ddev exec php bin/console app:seed +else + echo "⏭ Testdaten übersprungen." +fi + +echo "⏳ Cache leeren..." +ddev exec php bin/console cache:clear + +echo "⏳ Assets bauen..." +ddev exec npm run build + +echo "✅ Fertig!" \ No newline at end of file diff --git a/httpdocs/LICENSE b/httpdocs/LICENSE new file mode 100644 index 0000000..bc6b2a1 --- /dev/null +++ b/httpdocs/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/httpdocs/README.md b/httpdocs/README.md new file mode 100644 index 0000000..695e7fd --- /dev/null +++ b/httpdocs/README.md @@ -0,0 +1,37 @@ +# INSTALLATION + +cd httpdocs +ddev start => Läuft dann unter https://timetracking.ddev.site:8459 +ddev exec composer install +ddev exec npm install +sh 1-reset-and-seed.sh (Entweder mit oder ohne Testdaten) + +# Dann SCSS / JS bauen und beobachten: +ddev exec npm run watch + +# LIVE EINMALIG AUSFÜHREN: +GRANT ALL PRIVILEGES ON \`db_%\`.* TO 'deindbuser'@'%'; FLUSH PRIVILEGES; + +# Central Entity geändert → Migration erstellen + ausführen: +ddev exec php bin/console doctrine:migrations:diff --em=central --namespace=DoctrineMigrations +ddev exec php bin/console doctrine:migrations:migrate --em=central --no-interaction + +# Tenant Entity geändert → kein Migrations-Workflow. +sh reset-and-seed.sh +# Das legt die Tenant-DB per SchemaTool neu an. + +# Alle URLs anzeigen +ddev describe + +# Cache clear +ddev exec php bin/console cache:clear + +# WEBPACK +# Einmalig bauen (Dev) +ddev exec npm run dev + +# Watch-Modus (bei Änderungen automatisch neu bauen) +ddev exec npm run watch + +# Production Build +ddev exec npm run build diff --git a/httpdocs/assets/app.js b/httpdocs/assets/app.js new file mode 100644 index 0000000..6ab77a0 --- /dev/null +++ b/httpdocs/assets/app.js @@ -0,0 +1,11 @@ +/* + * Welcome to your app's main JavaScript file! + * + * We recommend including the built version of this JavaScript file + * (and its CSS file) in your base layout (base.html.twig). + */ + +// any CSS you import will output into a single css file (app.css in this case) +import './styles/main.scss'; +import './scripts/calendar.js'; +import './scripts/entries.js'; diff --git a/httpdocs/assets/scripts/calendar.js b/httpdocs/assets/scripts/calendar.js new file mode 100644 index 0000000..4d58918 --- /dev/null +++ b/httpdocs/assets/scripts/calendar.js @@ -0,0 +1,276 @@ +// assets/scripts/calendar.js +// Strings aus window.TT.i18n – keine hardcodierten deutschen Texte mehr + +function t(key) { + return window.TT?.i18n?.[key] ?? key; +} + +class WeekCalendar { + constructor() { + this.nav = document.querySelector('.week-nav'); + this.daysContainer = document.querySelector('.week-nav__days'); + this.calBtn = document.querySelector('.week-nav__cal'); + this.prevBtn = document.querySelector('.week-nav__arrow--prev'); + this.nextBtn = document.querySelector('.week-nav__arrow--next'); + this.header = document.querySelector('.tt-header'); + + const raw = this.nav?.dataset.activeDate; + this.activeDate = raw ? new Date(raw + 'T00:00:00') : new Date(); + this.today = new Date(); + this.today.setHours(0, 0, 0, 0); + + this.monthOpen = false; + this.monthDate = new Date(this.activeDate); + this.monthEl = null; + + if (!this.nav) return; + this.init(); + } + + get months() { return t('months') || []; } + get monthsShort() { return t('monthsShort') || []; } + get weekdays() { return t('weekdays') || []; } + get weekdaysShort() { return t('weekdaysShort') || []; } + + init() { + this.prevBtn?.addEventListener('click', e => { e.preventDefault(); this.navigateWeek(-1); }); + this.nextBtn?.addEventListener('click', e => { e.preventDefault(); this.navigateWeek(1); }); + this.calBtn?.addEventListener('click', e => { e.preventDefault(); this.toggleMonth(); }); + + this.daysContainer?.addEventListener('click', e => { + const dayEl = e.target.closest('.week-nav__day'); + if (!dayEl) return; + e.preventDefault(); + const dateStr = dayEl.dataset.date; + if (dateStr) this.goToDate(new Date(dateStr + 'T00:00:00')); + }); + } + + // ── Wochen-Navigation ───────────────────────────────────────────────────── + + navigateWeek(direction) { + const slideOut = direction > 0 ? 'slide-out-left' : 'slide-out-right'; + const slideIn = direction > 0 ? 'slide-in-right' : 'slide-in-left'; + + this.daysContainer.classList.add(slideOut); + + setTimeout(() => { + const newDate = new Date(this.activeDate); + newDate.setDate(newDate.getDate() + direction * 7); + this.activeDate = newDate; + this.renderWeekDays(); + this.updateHeaderMeta(); + + this.daysContainer.classList.remove(slideOut); + this.daysContainer.classList.add(slideIn); + requestAnimationFrame(() => requestAnimationFrame(() => { + this.daysContainer.classList.remove(slideIn); + })); + + window.history.pushState({}, '', `/week/${this.formatDate(this.getMonday(this.activeDate))}`); + window.entryManager?.loadEntriesForDate(this.formatDate(this.activeDate)); + }, 180); + } + + renderWeekDays() { + const monday = this.getMonday(this.activeDate); + this.daysContainer.innerHTML = ''; + + for (let i = 0; i < 7; i++) { + const d = new Date(monday); + d.setDate(d.getDate() + i); + const isActive = this.isSameDay(d, this.activeDate); + const isToday = this.isSameDay(d, this.today); + + // Führungsnull: padStart(2, '0') + const dayNum = String(d.getDate()).padStart(2, '0'); + const monthShort = this.monthsShort[d.getMonth()] ?? ''; + + const a = document.createElement('a'); + a.href = `/week/${this.formatDate(d)}`; + a.className = 'week-nav__day' + + (isActive ? ' week-nav__day--active' : '') + + (isToday ? ' week-nav__day--today' : ''); + a.dataset.date = this.formatDate(d); + a.innerHTML = ` + ${this.weekdaysShort[i] ?? ''} + ${dayNum}. ${monthShort} + `; + this.daysContainer.appendChild(a); + } + } + + goToDate(date) { + this.activeDate = date; + this.renderWeekDays(); + this.updateHeaderMeta(); + window.history.pushState({}, '', `/week/${this.formatDate(date)}`); + window.entryManager?.loadEntriesForDate(this.formatDate(date)); + if (this.monthOpen) this.closeMonth(); + } + + updateHeaderMeta() { + const dateEl = document.querySelector('.tt-header__date'); + const kwEl = document.querySelector('.tt-header__kw'); + + if (dateEl) { + const d = this.activeDate; + const day = d.getDate(); + const month = this.months[d.getMonth()] ?? ''; + const tomorrow = new Date(this.today); tomorrow.setDate(this.today.getDate() + 1); + const yesterday = new Date(this.today); yesterday.setDate(this.today.getDate() - 1); + + // JS getDay(): 0=So, 1=Mo...6=Sa → weekdays[0]=Montag, also index = getDay()-1, So=6 + const jsDay = d.getDay(); + const isoIdx = jsDay === 0 ? 6 : jsDay - 1; + const weekday = this.weekdays[isoIdx] ?? ''; + + let prefix; + if (this.isSameDay(d, this.today)) prefix = t('today'); + else if (this.isSameDay(d, tomorrow)) prefix = t('tomorrow'); + else if (this.isSameDay(d, yesterday)) prefix = t('yesterday'); + else prefix = weekday; + + dateEl.textContent = `${prefix}, ${day}. ${month}`; + document.title = `${prefix}, ${day}. ${month}`; + } + + if (kwEl) kwEl.textContent = `${t('weekLabel')} ${this.getWeekNumber(this.activeDate)}`; + } + + // ── Monats-Ansicht ──────────────────────────────────────────────────────── + + toggleMonth() { this.monthOpen ? this.closeMonth() : this.openMonth(); } + + openMonth() { + this.monthOpen = true; + this.monthDate = new Date(this.activeDate); + this.monthEl = document.createElement('div'); + this.monthEl.className = 'month-calendar month-calendar--hidden'; + this.header.appendChild(this.monthEl); + this.renderMonthGrid(); + requestAnimationFrame(() => requestAnimationFrame(() => { + this.monthEl.classList.remove('month-calendar--hidden'); + this.monthEl.classList.add('month-calendar--visible'); + })); + this.calBtn.classList.add('week-nav__cal--active'); + } + + closeMonth() { + if (!this.monthEl) return; + this.monthEl.classList.remove('month-calendar--visible'); + this.monthEl.classList.add('month-calendar--hidden'); + setTimeout(() => { this.monthEl?.remove(); this.monthEl = null; }, 280); + this.monthOpen = false; + this.calBtn.classList.remove('week-nav__cal--active'); + } + + renderMonthGrid() { + if (!this.monthEl) return; + + const year = this.monthDate.getFullYear(); + const month = this.monthDate.getMonth(); + + const firstDay = new Date(year, month, 1); + let startDow = firstDay.getDay(); + startDow = startDow === 0 ? 6 : startDow - 1; + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const daysInPrev = new Date(year, month, 0).getDate(); + + let html = ` +
+ + ${this.months[month] ?? ''} ${year} + + +
+
+
+ ${this.weekdaysShort.map(d => `${d}`).join('')} +
+
`; + + for (let i = startDow - 1; i >= 0; i--) + html += `${daysInPrev - i}`; + + for (let d = 1; d <= daysInMonth; d++) { + const date = new Date(year, month, d); + const isToday = this.isSameDay(date, this.today); + const isActive= this.isSameDay(date, this.activeDate); + const cls = 'month-day' + + (isToday ? ' month-day--today' : '') + + (isActive ? ' month-day--active' : ''); + html += `${d}`; + } + + const totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7; + for (let d = 1; d <= totalCells - startDow - daysInMonth; d++) + html += `${d}`; + + html += `
`; + this.monthEl.innerHTML = html; + + this.monthEl.querySelector('.month-nav-prev')?.addEventListener('click', () => this.navigateMonth(-1)); + this.monthEl.querySelector('.month-nav-next')?.addEventListener('click', () => this.navigateMonth(1)); + this.monthEl.querySelector('.month-calendar__close')?.addEventListener('click', () => this.closeMonth()); + this.monthEl.querySelectorAll('.month-day[data-date]').forEach(el => { + el.addEventListener('click', () => this.goToDate(new Date(el.dataset.date + 'T00:00:00'))); + }); + } + + navigateMonth(direction) { + const grid = this.monthEl?.querySelector('.month-calendar__grid'); + if (!grid) return; + const slideOut = direction > 0 ? 'slide-out-left' : 'slide-out-right'; + grid.classList.add(slideOut); + setTimeout(() => { + this.monthDate.setMonth(this.monthDate.getMonth() + direction); + this.renderMonthGrid(); + }, 160); + } + + // ── Hilfsfunktionen ─────────────────────────────────────────────────────── + + getMonday(date) { + const d = new Date(date); + const day = d.getDay(); + d.setDate(d.getDate() - day + (day === 0 ? -6 : 1)); + d.setHours(0, 0, 0, 0); + return d; + } + + isSameDay(a, b) { + return a.getFullYear() === b.getFullYear() + && a.getMonth() === b.getMonth() + && a.getDate() === b.getDate(); + } + + formatDate(date) { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + getWeekNumber(date) { + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + } +} + +document.addEventListener('DOMContentLoaded', () => { new WeekCalendar(); }); diff --git a/httpdocs/assets/scripts/crud.js b/httpdocs/assets/scripts/crud.js new file mode 100644 index 0000000..5111d11 --- /dev/null +++ b/httpdocs/assets/scripts/crud.js @@ -0,0 +1,473 @@ +// assets/scripts/crud.js +// Generisches CRUD-Handler für Kunden, Projekte, Leistungen + +const api = window.CRUD?.apiBase ?? ''; + +// ── Hilfsfunktionen ─────────────────────────────────────────────────────────── + +function buildClientOptions(selectedId = null) { + const clients = window.CRUD?.clients ?? []; + let html = ''; + clients.forEach(c => { + const sel = String(c.id) === String(selectedId) ? ' selected' : ''; + html += ``; + }); + return html; +} + +function rowPrefix() { + // Ermittelt den Entitätstyp aus der URL + if (location.pathname.includes('/clients')) return 'client'; + if (location.pathname.includes('/projects')) return 'project'; + if (location.pathname.includes('/services')) return 'service'; + return 'row'; +} + +// ── Create-Formular ─────────────────────────────────────────────────────────── + +function initCreateForm() { + const btnNew = document.getElementById('btn-new'); + const form = document.getElementById('crud-create'); + const btnSave = document.getElementById('btn-create-save'); + const btnCancel = document.getElementById('btn-create-cancel'); + + if (!btnNew || !form) return; + + btnNew.addEventListener('click', () => { + form.classList.toggle('crud-create--visible'); + document.getElementById('create-name')?.focus(); + }); + + btnCancel?.addEventListener('click', () => { + form.classList.remove('crud-create--visible'); + resetCreateForm(); + }); + + btnSave?.addEventListener('click', () => createEntity()); +} + +function resetCreateForm() { + const fields = ['create-name', 'create-note']; + fields.forEach(id => { + const el = document.getElementById(id); + if (el) el.value = ''; + }); + const rate = document.getElementById('create-rate'); + if (rate) rate.value = ''; + const billable = document.getElementById('create-billable'); + if (billable) billable.checked = true; + const client = document.getElementById('create-client'); + if (client) client.value = ''; +} + +async function createEntity() { + const name = document.getElementById('create-name')?.value?.trim(); + if (!name) { alert('Bitte einen Namen eingeben.'); return; } + + const body = buildCreateBody(); + + try { + const res = await fetch(api, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + alert(err.error ?? 'Fehler beim Speichern.'); + return; + } + + const data = await res.json(); + appendRowToList(data); + document.getElementById('crud-create')?.classList.remove('crud-create--visible'); + resetCreateForm(); + + } catch (err) { + console.error(err); + alert('Fehler beim Speichern.'); + } +} + +function buildCreateBody() { + const body = { + name: document.getElementById('create-name')?.value?.trim(), + note: document.getElementById('create-note')?.value || null, + }; + + // Kunden-spezifisch + const rate = document.getElementById('create-rate'); + if (rate) body.hourlyRate = rate.value || null; + + // Projekt-spezifisch + const client = document.getElementById('create-client'); + if (client) body.clientId = parseInt(client.value) || null; + + // Leistungs-spezifisch + const billable = document.getElementById('create-billable'); + if (billable) body.billable = billable.checked; + + return body; +} + +// ── Liste: Event Delegation ──────────────────────────────────────────────────── + +function initList() { + const list = document.getElementById('crud-list'); + if (!list) return; + + list.addEventListener('click', e => { + const actionEl = e.target.closest('[data-action]'); + if (!actionEl) return; + + const action = actionEl.dataset.action; + const row = e.target.closest('.crud-row'); + if (!row) return; + + switch (action) { + case 'edit': openEdit(row); break; + case 'delete': deleteRow(row); break; + case 'save': saveEdit(row); break; + case 'cancel': closeEdit(row); break; + case 'unarchive': unarchiveRow(row); break; + } + }); +} + +// ── Inline Edit ─────────────────────────────────────────────────────────────── + +function openEdit(row) { + row.querySelector('.crud-row__display').hidden = true; + row.querySelector('.crud-row__edit').hidden = false; + row.querySelector('.edit-name')?.focus(); +} + +function closeEdit(row) { + row.querySelector('.crud-row__display').hidden = false; + row.querySelector('.crud-row__edit').hidden = true; +} + +async function saveEdit(row) { + const id = row.dataset.id; + const name = row.querySelector('.edit-name')?.value?.trim(); + + if (!name) { alert('Bitte einen Namen eingeben.'); return; } + + const body = buildEditBody(row); + + try { + const res = await fetch(`${api}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!res.ok) { alert('Fehler beim Speichern.'); return; } + + const data = await res.json(); + updateRowDisplay(row, data); + closeEdit(row); + + } catch (err) { + console.error(err); + alert('Fehler beim Speichern.'); + } +} + +function buildEditBody(row) { + const body = { + name: row.querySelector('.edit-name')?.value?.trim(), + note: row.querySelector('.edit-note')?.value || null, + }; + + // Kunden + const rate = row.querySelector('.edit-rate'); + if (rate) body.hourlyRate = rate.value || null; + + // Projekt + const client = row.querySelector('.edit-client'); + if (client) body.clientId = parseInt(client.value) || null; + + // Leistung + const billable = row.querySelector('.edit-billable'); + if (billable) body.billable = billable.checked; + + return body; +} + +function updateRowDisplay(row, data) { + const nameEl = row.querySelector('.crud-row__name'); + const metaEl = row.querySelector('.crud-row__meta'); + + if (nameEl) nameEl.textContent = data.name; + + // Kunden: Meta-Text unverändert (Projektanzahl ändert sich nicht) + // Projekte: Client-Name aktualisieren + if (data.clientName && metaEl) metaEl.textContent = data.clientName; + + // data-Attribute aktualisieren + row.dataset.name = data.name; + if (data.hourlyRate !== undefined) row.dataset.rate = data.hourlyRate ?? ''; + if (data.clientId !== undefined) row.dataset.clientId = data.clientId; + if (data.billable !== undefined) row.dataset.billable = data.billable ? '1' : '0'; + if (data.note !== undefined) row.dataset.note = data.note ?? ''; + + // Edit-Felder aktualisieren + const editName = row.querySelector('.edit-name'); + if (editName) editName.value = data.name; + + const editNote = row.querySelector('.edit-note'); + if (editNote) editNote.value = data.note ?? ''; + + const editRate = row.querySelector('.edit-rate'); + if (editRate) editRate.value = data.hourlyRate ?? ''; + + const editBillable = row.querySelector('.edit-billable'); + if (editBillable) editBillable.checked = !!data.billable; +} + +// ── Delete ──────────────────────────────────────────────────────────────────── + +async function deleteRow(row) { + if (!confirm('Wirklich löschen?')) return; + + try { + const res = await fetch(`${api}/${row.dataset.id}`, { method: 'DELETE' }); + + if (res.status === 409) { + if (confirm('Dieser Eintrag hat abhängige Zeiteinträge und kann nicht gelöscht werden.\nStattdessen archivieren?')) { + await archiveRow(row); + } + return; + } + + if (!res.ok) { alert('Fehler beim Löschen.'); return; } + + row.classList.add('crud-row--removing'); + setTimeout(() => row.remove(), 280); + + } catch { + alert('Fehler beim Löschen.'); + } +} + +async function archiveRow(row) { + try { + const res = await fetch(`${api}/${row.dataset.id}/archive`, { method: 'PATCH' }); + if (!res.ok) { alert('Fehler beim Archivieren.'); return; } + + row.dataset.archived = '1'; + row.classList.add('crud-row--archived'); + updateRowArchivedState(row, true); + filterByTab(document.querySelector('.crud-tab--active')?.dataset.tab ?? 'active'); + + } catch { + alert('Fehler beim Archivieren.'); + } +} + +async function unarchiveRow(row) { + try { + const res = await fetch(`${api}/${row.dataset.id}/unarchive`, { method: 'PATCH' }); + if (!res.ok) { alert('Fehler beim Wiederherstellen.'); return; } + + row.dataset.archived = '0'; + row.classList.remove('crud-row--archived'); + updateRowArchivedState(row, false); + filterByTab(document.querySelector('.crud-tab--active')?.dataset.tab ?? 'active'); + + } catch { + alert('Fehler beim Wiederherstellen.'); + } +} + +function updateRowArchivedState(row, archived) { + const actions = row.querySelector('.crud-row__actions'); + if (!actions) return; + + if (archived) { + actions.innerHTML = ` + `; + row.querySelector('.crud-row__edit')?.remove(); + } else { + actions.innerHTML = ` + + `; + } +} + +function filterByTab(tab) { + document.querySelectorAll('#crud-list .crud-row').forEach(row => { + row.hidden = tab === 'active' + ? row.dataset.archived === '1' + : row.dataset.archived === '0'; + }); +} + +function initTabs() { + const tabs = document.querySelectorAll('.crud-tab'); + if (!tabs.length) return; + + filterByTab('active'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('crud-tab--active')); + tab.classList.add('crud-tab--active'); + filterByTab(tab.dataset.tab); + + const btnNew = document.getElementById('btn-new'); + if (btnNew) btnNew.hidden = tab.dataset.tab === 'archived'; + }); + }); +} + +// ── Neue Zeile einfügen ─────────────────────────────────────────────────────── + +function appendRowToList(data) { + const list = document.getElementById('crud-list'); + if (!list) return; + + const html = buildRowHTML(data); + + // Services haben Gruppen → in die richtige Gruppe einfügen + if (data.billable !== undefined) { + const groupLabel = data.billable ? 'Verrechenbar' : 'Nicht-verrechenbar'; + let targetGroup = null; + + list.querySelectorAll('.crud-list__group').forEach(g => { + if (g.querySelector('.crud-list__group-label')?.textContent === groupLabel) { + targetGroup = g; + } + }); + + if (targetGroup) { + targetGroup.insertAdjacentHTML('beforeend', html); + } else { + // Gruppe existiert noch nicht → neu anlegen + const groupHtml = `
${groupLabel}
${html}
`; + if (!data.billable) { + // Nicht-verrechenbar immer ans Ende + list.insertAdjacentHTML('beforeend', groupHtml); + } else { + // Verrechenbar vor die erste existierende Gruppe + const firstGroup = list.querySelector('.crud-list__group'); + firstGroup + ? firstGroup.insertAdjacentHTML('beforebegin', groupHtml) + : list.insertAdjacentHTML('beforeend', groupHtml); + } + } + } else { + list.insertAdjacentHTML('beforeend', html); + } + + const prefix = rowPrefix(); + const el = document.getElementById(`${prefix}-${data.id}`); + if (el) { + requestAnimationFrame(() => requestAnimationFrame(() => { + el.classList.remove('crud-row--new'); + })); + } +} + +function buildRowHTML(data) { + const prefix = rowPrefix(); + let metaHtml = ''; + let editFields = ''; + + // Kunden + if (data.projectCount !== undefined) { + const c = data.projectCount; + metaHtml = `${c} ${c === 1 ? 'Projekt' : 'Projekte'}`; + editFields = ` + +
+ +
+ + +
+ +
`; + } + + // Projekte + if (data.clientName !== undefined && data.projectCount === undefined) { + metaHtml = `${data.clientName}`; + editFields = ` + +
+ +
+ +
`; + } + + // Leistungen + if (data.billable !== undefined) { + editFields = ` + +
+ +
+ +
+ +
`; + } + + return ` +
+ +
+
+ ${data.name} + ${metaHtml} +
+
+ + +
+
+ + +
`; +} + +// ── Init ────────────────────────────────────────────────────────────────────── + +document.addEventListener('DOMContentLoaded', () => { + initCreateForm(); + initList(); + initTabs(); +}); diff --git a/httpdocs/assets/scripts/duration.js b/httpdocs/assets/scripts/duration.js new file mode 100644 index 0000000..702ef6f --- /dev/null +++ b/httpdocs/assets/scripts/duration.js @@ -0,0 +1,96 @@ +// assets/scripts/duration.js +// Zentrale Logik für Zeiteingabe – wird von entries.js importiert + +// ── Konfiguration ───────────────────────────────────────────────────────────── +// Auf false setzen um Viertelstunden-Runden zu deaktivieren +export const DURATION_CONFIG = { + roundToQuarter: true, +}; + +// ── Parser ──────────────────────────────────────────────────────────────────── + +/** + * Parst Zeiteingaben in Minuten. + * + * Unterstützte Formate: + * "1:30" → 90 (Stunden:Minuten) + * "8 12" → 240 (von 8 bis 12 Uhr) + * "1,75" → 105 (Dezimalstunden mit Komma) + * "1.75" → 105 (Dezimalstunden mit Punkt) + * "2" → 120 (nur Stunden als ganze Zahl) + * "0:00" → 0 (Stopp/Reset) + */ +export function parseDuration(input) { + input = String(input).trim(); + + if (!input || input === '0' || input === '0:00') return 0; + + // "8 12" → von 8 bis 12 Uhr + if (/^\d+\s+\d+$/.test(input)) { + const parts = input.split(/\s+/).map(Number); + const minutes = (parts[1] - parts[0]) * 60; + return Math.max(0, minutes); + } + + // "1:30" → Stunden:Minuten + if (input.includes(':')) { + const [h, m] = input.split(':').map(s => parseInt(s) || 0); + return h * 60 + m; + } + + // "1,75" oder "1.75" → Dezimalstunden + if (input.includes(',') || input.includes('.')) { + const hours = parseFloat(input.replace(',', '.')); + return isNaN(hours) ? 0 : Math.round(hours * 60); + } + + // "2" → 2 Stunden + const hours = parseInt(input); + return isNaN(hours) ? 0 : hours * 60; +} + +// ── Rounding ────────────────────────────────────────────────────────────────── + +/** + * Rundet Minuten auf die nächste Viertelstunde auf. + * 0 bleibt 0 (Stopp). + */ +export function roundToQuarter(minutes) { + if (!DURATION_CONFIG.roundToQuarter) return minutes; + if (minutes === 0) return 0; + const interval = window.TT?.trackingInterval ?? 15; + return Math.ceil(minutes / interval) * interval; +} + +// ── Formatter ───────────────────────────────────────────────────────────────── + +export function formatMinutes(minutes) { + const h = Math.floor(minutes / 60); + const m = minutes % 60; + return `${h}:${String(m).padStart(2, '0')}`; +} + +// ── Blur-Handler (global, per Event Delegation) ─────────────────────────────── +// Reagiert auf blur an allen Dauer-Inputs, egal ob server-gerendert oder JS-erstellt + +export function initDurationBlurHandler() { + document.addEventListener('blur', e => { + if (!(e.target instanceof Element)) return; + if (!e.target.matches('#create-duration, .edit-duration')) return; + + const raw = e.target.value; + const minutes = roundToQuarter(parseDuration(raw)); + e.target.value = formatMinutes(minutes); + }, true); // capture=true, weil blur nicht bubbled +} + +/** + * Validiert eine Dauer in Minuten. + * > 1440 (24h) → error + * > 480 (8h) → warn + */ +export function validateDuration(minutes) { + if (minutes > 1440) return { status: 'error' }; + if (minutes > 480) return { status: 'warn' }; + return { status: 'ok' }; +} diff --git a/httpdocs/assets/scripts/entries.js b/httpdocs/assets/scripts/entries.js new file mode 100644 index 0000000..9b2d13f --- /dev/null +++ b/httpdocs/assets/scripts/entries.js @@ -0,0 +1,441 @@ +// assets/scripts/entries.js +import { parseDuration, roundToQuarter, formatMinutes, initDurationBlurHandler, validateDuration } from './duration.js'; + +const LAST_PROJECT_KEY = 'tt_last_project_id'; +const LAST_SERVICE_KEY = 'tt_last_service_id'; + +function t(key) { + return window.TT?.i18n?.[key] ?? key; +} + +function buildProjectOptions(selectedId = null) { + const groups = {}; + (window.TT?.projects ?? []).forEach(p => { + if (!groups[p.clientName]) groups[p.clientName] = []; + groups[p.clientName].push(p); + }); + + let html = ``; + for (const [client, projects] of Object.entries(groups)) { + html += ``; + projects.forEach(p => { + const sel = String(p.id) === String(selectedId) ? ' selected' : ''; + html += ``; + }); + html += ''; + } + return html; +} + +function buildServiceOptions(selectedId = null) { + const billable = (window.TT?.services ?? []).filter(s => s.billable); + const notBillable = (window.TT?.services ?? []).filter(s => !s.billable); + + let html = ``; + + if (billable.length) { + html += ``; + billable.forEach(s => { + const sel = String(s.id) === String(selectedId) ? ' selected' : ''; + html += ``; + }); + html += ''; + } + + if (notBillable.length) { + html += ``; + notBillable.forEach(s => { + const sel = String(s.id) === String(selectedId) ? ' selected' : ''; + html += ``; + }); + html += ''; + } + + return html; +} + +function buildEntryRowHTML(entry, animate = false) { + const servicePart = entry.serviceName ? ` / ${entry.serviceName}` : ''; + const notePart = entry.note ? `
${entry.note}
` : ''; + + return ` +
+ +
+
+
${entry.clientName} / ${entry.projectName}${servicePart}
+ ${notePart} +
+
+ ${entry.durationFormatted} + + +
+
+ + +
`; +} + +class EntryManager { + constructor() { + this.list = document.getElementById('entry-list'); + this.emptyState = document.getElementById('empty-state'); + + if (!this.list) return; + + const cp = document.getElementById('create-project'); + const cs = document.getElementById('create-service'); + + document.getElementById('create-service')?.addEventListener('change', e => { + saveLastService(e.target.value); + }); + + document.getElementById('create-project')?.addEventListener('change', e => { + saveLastProject(e.target.value); + }); + + if (cp) { + const lastProject = getLastProject(); + cp.innerHTML = buildProjectOptions(lastProject); + if (lastProject) cp.value = lastProject; + } + if (cs) { + const lastService = getLastService(); + cs.innerHTML = buildServiceOptions(lastService); + if (lastService) cs.value = lastService; + } + + this.list.querySelectorAll('.entry-row').forEach(row => { + const ep = row.querySelector('.edit-project'); + const es = row.querySelector('.edit-service'); + if (ep) ep.innerHTML = buildProjectOptions(row.dataset.projectId); + if (es) es.innerHTML = buildServiceOptions(row.dataset.serviceId); + }); + + this.list.addEventListener('click', e => this.handleListClick(e)); + document.getElementById('btn-create')?.addEventListener('click', () => this.createEntry()); + } + + handleListClick(e) { + const actionEl = e.target.closest('[data-action]'); + if (!actionEl) return; + const action = actionEl.dataset.action; + const row = e.target.closest('.entry-row'); + if (!row) return; + + switch (action) { + case 'edit': this.openEdit(row); break; + case 'delete': this.deleteEntry(row); break; + case 'save': this.saveEdit(row); break; + case 'cancel': this.closeEdit(row); break; + } + } + + async createEntry() { + const durationRaw = document.getElementById('create-duration')?.value ?? '0:00'; + const projectId = document.getElementById('create-project')?.value; + const serviceId = document.getElementById('create-service')?.value; + const note = document.getElementById('create-note')?.value; + + if (!projectId) { alert(t('errorNoProject')); return; } + + const duration = formatMinutes(roundToQuarter(parseDuration(durationRaw))); + + if (duration === '0:00') { + alert(t('errorZeroDuration')); + return; + } + + const rawMinutes = roundToQuarter(parseDuration(durationRaw)); + const validation = validateDuration(rawMinutes); + if (validation.status === 'error') { alert(t('errorDurationTooLong')); return; } + if (validation.status === 'warn' && !confirm(t('warnDurationLong'))) return; + + try { + const res = await fetch('/api/entries', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + date: window.TT.activeDate, + duration, + projectId: parseInt(projectId), + serviceId: serviceId ? parseInt(serviceId) : null, + note: note || null, + }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + console.error('API Fehler:', res.status, err); + alert(t('errorSave') + (err.error ? `\n${err.error}` : '')); + return; + } + + const data = await res.json(); + this.addEntryToDOM(data.entry); + this.updateTotal(data.totalDuration); + this.resetCreateForm(); + + } catch (err) { + console.error('Netzwerkfehler:', err); + alert(t('errorSave')); + } + } + + addEntryToDOM(entry) { + this.hideEmptyState(); + + let items = document.getElementById('entry-items'); + if (!items) { + items = document.createElement('div'); + items.className = 'entry-list__items'; + items.id = 'entry-items'; + this.list.prepend(items); + } + + items.insertAdjacentHTML('beforeend', buildEntryRowHTML(entry, true)); + + const el = document.getElementById(`entry-${entry.id}`); + requestAnimationFrame(() => requestAnimationFrame(() => { + el?.classList.remove('entry-row--new'); + })); + } + + resetCreateForm() { + const d = document.getElementById('create-duration'); + const p = document.getElementById('create-project'); + const s = document.getElementById('create-service'); + const n = document.getElementById('create-note'); + if (d) d.value = '0:00'; + if (n) n.value = ''; + if (p) p.value = getLastProject() ?? ''; + if (s) s.value = getLastService() ?? ''; + } + + openEdit(row) { + row.querySelector('.entry-row__display').hidden = true; + row.querySelector('.entry-row__edit').hidden = false; + row.querySelector('.edit-duration')?.focus(); + } + + closeEdit(row) { + row.querySelector('.entry-row__display').hidden = false; + row.querySelector('.entry-row__edit').hidden = true; + } + + async saveEdit(row) { + const id = row.dataset.id; + const durationRaw = row.querySelector('.edit-duration')?.value ?? '0:00'; + const projectId = row.querySelector('.edit-project')?.value; + const serviceId = row.querySelector('.edit-service')?.value; + const note = row.querySelector('.edit-note')?.value; + + if (!projectId) { alert(t('errorNoProject')); return; } + + const duration = formatMinutes(roundToQuarter(parseDuration(durationRaw))); + + if (duration === '0:00') { + alert(t('errorZeroDuration')); + return; + } + + const rawMinutes = roundToQuarter(parseDuration(durationRaw)); + const validation = validateDuration(rawMinutes); + if (validation.status === 'error') { alert(t('errorDurationTooLong')); return; } + if (validation.status === 'warn' && !confirm(t('warnDurationLong'))) return; + + try { + const res = await fetch(`/api/entries/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + duration, + projectId: parseInt(projectId), + serviceId: serviceId ? parseInt(serviceId) : null, + note: note || null, + }), + }); + + if (!res.ok) { + console.error('PATCH fehlgeschlagen:', res.status); + alert(t('errorSave')); + return; + } + + const data = await res.json(); + this.updateRowDisplay(row, data.entry); + this.updateTotal(data.totalDuration); + this.closeEdit(row); + + } catch (err) { + console.error('saveEdit Fehler:', err); + alert(t('errorSave')); + } + } + + updateRowDisplay(row, entry) { + const servicePart = entry.serviceName ? ` / ${entry.serviceName}` : ''; + row.querySelector('.entry-row__title').textContent = + `${entry.clientName} / ${entry.projectName}${servicePart}`; + + row.querySelector('.entry-row__note')?.remove(); + if (entry.note) { + const noteEl = document.createElement('div'); + noteEl.className = 'entry-row__note'; + noteEl.textContent = entry.note; + row.querySelector('.entry-row__info').appendChild(noteEl); + } + + row.querySelector('.entry-row__badge').textContent = entry.durationFormatted; + row.dataset.duration = entry.duration; + row.dataset.projectId = entry.projectId; + row.dataset.serviceId = entry.serviceId ?? ''; + row.dataset.note = entry.note ?? ''; + + row.querySelector('.edit-duration').value = entry.durationFormatted; + row.querySelector('.edit-project').innerHTML = buildProjectOptions(entry.projectId); + row.querySelector('.edit-service').innerHTML = buildServiceOptions(entry.serviceId); + row.querySelector('.edit-note').value = entry.note ?? ''; + } + + async deleteEntry(row) { + if (!confirm(t('confirmDelete'))) return; + + try { + const res = await fetch(`/api/entries/${row.dataset.id}`, { method: 'DELETE' }); + if (!res.ok) { alert(t('errorDelete')); return; } + + const data = await res.json(); + row.classList.add('entry-row--removing'); + setTimeout(() => { + row.remove(); + this.updateTotal(data.totalDuration); + this.checkIfEmpty(); + }, 280); + + } catch { alert(t('errorDelete')); } + } + + async loadEntriesForDate(dateStr) { + window.TT.activeDate = dateStr; + + try { + this.list.classList.add('entry-list--fading'); + await new Promise(r => setTimeout(r, 180)); + + const res = await fetch(`/api/entries?date=${dateStr}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + + this.renderEntries(data.entries, data.totalDuration); + } catch (err) { + console.error(t('errorLoad'), err); + } finally { + this.list.classList.remove('entry-list--fading'); + } + } + + renderEntries(entries, totalDuration) { + if (!entries.length) { + this.list.innerHTML = `
+

${t('noEntries')}

`; + this.emptyState = this.list.querySelector('#empty-state'); + return; + } + + let html = '
'; + entries.forEach(e => { html += buildEntryRowHTML(e, false); }); + html += `
+ ${totalDuration}
`; + + this.list.innerHTML = html; + this.emptyState = null; + + this.list.querySelectorAll('.entry-row').forEach(row => { + row.querySelector('.edit-project').innerHTML = buildProjectOptions(row.dataset.projectId); + row.querySelector('.edit-service').innerHTML = buildServiceOptions(row.dataset.serviceId); + }); + } + + updateTotal(totalDuration) { + let footer = document.getElementById('entry-footer'); + if (!footer) { + footer = document.createElement('div'); + footer.className = 'entry-list__footer'; + footer.id = 'entry-footer'; + this.list.appendChild(footer); + } + footer.innerHTML = `${totalDuration}`; + } + + hideEmptyState() { this.emptyState?.remove(); this.emptyState = null; } + + checkIfEmpty() { + const items = document.getElementById('entry-items'); + if (items && !items.children.length) { + items.remove(); + document.getElementById('entry-footer')?.remove(); + this.list.innerHTML = `
+

${t('noEntries')}

`; + this.emptyState = this.list.querySelector('#empty-state'); + } + } +} + +function saveLastProject(projectId) { + if (projectId) localStorage.setItem(LAST_PROJECT_KEY, projectId); +} + +function getLastProject() { + return localStorage.getItem(LAST_PROJECT_KEY); +} + +function saveLastService(serviceId) { + if (serviceId) localStorage.setItem(LAST_SERVICE_KEY, serviceId); +} + +function getLastService() { + return localStorage.getItem(LAST_SERVICE_KEY); +} + +window.entryManager = null; +document.addEventListener('DOMContentLoaded', () => { + initDurationBlurHandler(); + window.entryManager = new EntryManager(); +}); diff --git a/httpdocs/assets/scripts/registration.js b/httpdocs/assets/scripts/registration.js new file mode 100644 index 0000000..318b64c --- /dev/null +++ b/httpdocs/assets/scripts/registration.js @@ -0,0 +1,87 @@ +// assets/scripts/registration.js + +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('register-form'); + const companyInput = document.getElementById('companyName'); + const slugPreview = document.getElementById('slug-preview'); + const submitBtn = document.getElementById('submit-btn'); + const errorBox = document.getElementById('register-errors'); + const appDomain = window.REGISTER_APP_DOMAIN ?? ''; + + // ── Slug-Vorschau ───────────────────────────────────────────────────────── + let debounce = null; + companyInput?.addEventListener('input', () => { + clearTimeout(debounce); + debounce = setTimeout(async () => { + const value = companyInput.value.trim(); + if (!value) { slugPreview.textContent = ''; return; } + + try { + const res = await fetch('/api/register/preview-slug', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ companyName: value }), + }); + const data = await res.json(); + slugPreview.textContent = data.slug ? data.slug + '.' + appDomain : '–'; + } catch { + slugPreview.textContent = ''; + } + }, 350); + }); + + // ── Formular absenden ───────────────────────────────────────────────────── + form?.addEventListener('submit', async (e) => { + e.preventDefault(); + errorBox.innerHTML = ''; + submitBtn.disabled = true; + submitBtn.textContent = 'Wird gesendet …'; + + const payload = { + companyName: document.getElementById('companyName').value, + email: document.getElementById('email').value, + firstName: document.getElementById('firstName').value, + lastName: document.getElementById('lastName').value, + password: document.getElementById('password').value, + passwordRepeat: document.getElementById('passwordRepeat').value, + }; + + try { + const res = await fetch('/api/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + + if (res.ok) { + document.querySelector('.register-page').innerHTML = ` +
+
+

Fast geschafft!

+

+ Wir haben eine Bestätigungs-E-Mail an + ${payload.email} geschickt. +

+

+ Bitte klicke auf den Link in der E-Mail um dein Konto zu aktivieren. + Der Link ist 24 Stunden gültig. +

+
+ `; + } else { + (data.errors ?? ['Unbekannter Fehler.']).forEach(msg => { + const p = document.createElement('p'); + p.textContent = msg; + errorBox.appendChild(p); + }); + submitBtn.disabled = false; + submitBtn.textContent = 'Konto erstellen'; + } + } catch { + errorBox.innerHTML = '

Verbindungsfehler. Bitte versuche es erneut.

'; + submitBtn.disabled = false; + submitBtn.textContent = 'Konto erstellen'; + } + }); +}); \ No newline at end of file diff --git a/httpdocs/assets/scripts/team.js b/httpdocs/assets/scripts/team.js new file mode 100644 index 0000000..8664d5f --- /dev/null +++ b/httpdocs/assets/scripts/team.js @@ -0,0 +1,223 @@ +// team.js +document.addEventListener('DOMContentLoaded', () => { + + // ── Tabs ───────────────────────────────────────────────────────────────────── + document.querySelectorAll('.crud-tab').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('.crud-tab').forEach(t => + t.classList.toggle('crud-tab--active', t === tab) + ); + document.querySelectorAll('[data-tab-panel]').forEach(panel => { + panel.hidden = panel.dataset.tabPanel !== tab.dataset.tab; + }); + }); + }); + + // ── Einlade-Modal ───────────────────────────────────────────────────────────── + const modal = document.getElementById('team-modal'); + const errorsBox = document.getElementById('team-modal-errors'); + + const openModal = () => { modal.hidden = false; }; + const closeModal = () => { + modal.hidden = true; + errorsBox.hidden = true; + ['inv-firstName', 'inv-lastName', 'inv-email'].forEach(id => { + document.getElementById(id).value = ''; + }); + const defaultRole = modal.querySelector('input[name="inv-role"][value="member"]'); + if (defaultRole) defaultRole.checked = true; + }; + + document.getElementById('team-invite-btn').addEventListener('click', openModal); + document.getElementById('team-modal-close').addEventListener('click', closeModal); + document.getElementById('team-modal-cancel').addEventListener('click', closeModal); + modal.addEventListener('click', e => { if (e.target === modal) closeModal(); }); + + document.getElementById('team-modal-submit').addEventListener('click', async () => { + const payload = { + firstName: document.getElementById('inv-firstName').value.trim(), + lastName: document.getElementById('inv-lastName').value.trim(), + email: document.getElementById('inv-email').value.trim(), + role: modal.querySelector('input[name="inv-role"]:checked')?.value ?? 'member', + }; + + const res = await fetch('/api/team/invite', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + + if (!res.ok) { + errorsBox.hidden = false; + errorsBox.innerHTML = '
    ' + (data.errors ?? [data.error]).map(e => `
  • ${e}
  • `).join('') + '
'; + return; + } + + closeModal(); + window.location.reload(); + }); + + // ── Listen-Delegation: aktive User + Einladungen ─────────────────────────── + const list = document.getElementById('team-list'); + if (list) { + list.addEventListener('click', e => { + const actionEl = e.target.closest('[data-action]'); + if (!actionEl) return; + + const action = actionEl.dataset.action; + const row = e.target.closest('.crud-row'); + if (!row) return; + + switch (action) { + case 'edit': openEdit(row); break; + case 'save': saveEdit(row); break; + case 'cancel': closeEdit(row); break; + case 'delete': deleteMember(row); break; + case 'delete-invite': deleteInvite(actionEl.dataset.id, row); break; + } + }); + } + + // ── Listen-Delegation: archivierte User ─────────────────────────────────── + const archivedList = document.getElementById('team-list-archived'); + if (archivedList) { + archivedList.addEventListener('click', e => { + const actionEl = e.target.closest('[data-action]'); + if (!actionEl) return; + const row = e.target.closest('.crud-row'); + if (!row) return; + + if (actionEl.dataset.action === 'unarchive') { + unarchiveMember(row); + } + }); + } + + // ── Inline Edit ─────────────────────────────────────────────────────────── + function openEdit(row) { + row.querySelector('.crud-row__display').hidden = true; + row.querySelector('.crud-row__edit').hidden = false; + row.querySelector('.edit-first-name')?.focus(); + } + + function closeEdit(row) { + row.querySelector('.crud-row__display').hidden = false; + row.querySelector('.crud-row__edit').hidden = true; + + // Felder auf ursprüngliche Werte zurücksetzen + row.querySelector('.edit-first-name').value = row.dataset.firstName ?? ''; + row.querySelector('.edit-last-name').value = row.dataset.lastName ?? ''; + row.querySelector('.edit-email').value = row.dataset.email ?? ''; + row.querySelector('.edit-note').value = row.dataset.note ?? ''; + + const currentRole = row.dataset.role; + row.querySelectorAll('.edit-role').forEach(radio => { + radio.checked = radio.value === currentRole; + }); + } + + async function saveEdit(row) { + const id = row.dataset.id; + const firstName = row.querySelector('.edit-first-name').value.trim(); + const lastName = row.querySelector('.edit-last-name').value.trim(); + const email = row.querySelector('.edit-email').value.trim(); + const note = row.querySelector('.edit-note').value || null; + const role = row.querySelector('.edit-role:checked')?.value ?? row.dataset.role; + + const res = await fetch(`/api/team/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ firstName, lastName, email, note, role }), + }); + + if (!res.ok) { + const data = await res.json(); + alert((data.errors ?? [data.error]).join('\n')); + return; + } + + const data = await res.json(); + updateDisplay(row, data); + closeEdit(row); + } + + function updateDisplay(row, data) { + row.querySelector('.crud-row__name').textContent = data.fullName; + row.querySelector('.crud-row__meta').textContent = `(${data.roleLabel})`; + + row.dataset.firstName = data.firstName; + row.dataset.lastName = data.lastName; + row.dataset.email = data.email; + row.dataset.note = data.note ?? ''; + row.dataset.role = data.role; + + // Edit-Felder aktualisieren + row.querySelector('.edit-first-name').value = data.firstName; + row.querySelector('.edit-last-name').value = data.lastName; + row.querySelector('.edit-email').value = data.email; + row.querySelector('.edit-note').value = data.note ?? ''; + row.querySelectorAll('.edit-role').forEach(radio => { + radio.checked = radio.value === data.role; + }); + } + + // ── Delete ──────────────────────────────────────────────────────────────── + async function deleteMember(row) { + if (!confirm('Wirklich entfernen?')) return; + + const id = row.dataset.id; + const res = await fetch(`/api/team/${id}`, { method: 'DELETE' }); + + if (res.status === 409) { + if (confirm('Dieser Benutzer hat Zeiteinträge und kann nicht gelöscht werden.\nStattdessen archivieren?')) { + await archiveMember(row); + } + return; + } + + if (!res.ok) { + const data = await res.json(); + alert(data.error ?? 'Fehler beim Löschen.'); + return; + } + + row.classList.add('crud-row--removing'); + setTimeout(() => row.remove(), 280); + } + + async function deleteInvite(id, row) { + if (!confirm('Einladung zurückziehen?')) return; + + const res = await fetch(`/api/team/invite/${id}`, { method: 'DELETE' }); + if (!res.ok) { + const data = await res.json(); + alert(data.error ?? 'Fehler'); + return; + } + + row.classList.add('crud-row--removing'); + setTimeout(() => row.remove(), 280); + } + + // ── Archive / Unarchive ─────────────────────────────────────────────────── + async function archiveMember(row) { + const id = row.dataset.id; + const res = await fetch(`/api/team/${id}/archive`, { method: 'PATCH' }); + + if (!res.ok) { alert('Fehler beim Archivieren.'); return; } + + row.classList.add('crud-row--removing'); + setTimeout(() => window.location.reload(), 280); + } + + async function unarchiveMember(row) { + const id = row.dataset.id; + const res = await fetch(`/api/team/${id}/unarchive`, { method: 'PATCH' }); + + if (!res.ok) { alert('Fehler beim Wiederherstellen.'); return; } + + row.classList.add('crud-row--removing'); + setTimeout(() => window.location.reload(), 280); + } +}); \ No newline at end of file diff --git a/httpdocs/assets/styles/atoms/_buttons.scss b/httpdocs/assets/styles/atoms/_buttons.scss new file mode 100644 index 0000000..5548d72 --- /dev/null +++ b/httpdocs/assets/styles/atoms/_buttons.scss @@ -0,0 +1,67 @@ +@use 'variables' as *; + +// ─── Base Button ───────────────────────────────────────────────────────────── +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: $space-2; + padding: $space-2 $space-5; + font-family: $font-family-base; + font-size: $font-size-base; + font-weight: $font-weight-medium; + line-height: 1.4; + border: none; + border-radius: $radius-pill; + cursor: pointer; + text-decoration: none; + transition: + background-color $transition-fast, + box-shadow $transition-fast, + transform $transition-fast; + white-space: nowrap; + + &:active { + transform: translateY(1px); + } + + &:focus-visible { + outline: 2px solid $color-primary; + outline-offset: 3px; + } +} + +// ─── Variants ──────────────────────────────────────────────────────────────── +.btn-primary { + background: linear-gradient(135deg, $color-accent-light, $color-accent); + color: $color-white; + box-shadow: $shadow-button; + + &:hover { + background: linear-gradient(135deg, $color-accent, $color-accent-dark); + box-shadow: 0 3px 12px rgba(240, 165, 0, 0.45); + } +} + +.btn-secondary { + background: transparent; + color: $color-text-muted; + padding-left: $space-3; + padding-right: $space-3; + + &:hover { + color: $color-text-dark; + text-decoration: underline; + } +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.15); + color: $color-white; + border-radius: $radius-sm; + padding: $space-1 $space-3; + + &:hover { + background: rgba(255, 255, 255, 0.25); + } +} diff --git a/httpdocs/assets/styles/atoms/_inputs.scss b/httpdocs/assets/styles/atoms/_inputs.scss new file mode 100644 index 0000000..4031ca7 --- /dev/null +++ b/httpdocs/assets/styles/atoms/_inputs.scss @@ -0,0 +1,89 @@ +@use 'variables' as *; + +// ─── Base Input ────────────────────────────────────────────────────────────── +.input, +.select, +.textarea { + display: block; + width: 100%; + padding: $space-2 $space-3; + font-family: $font-family-base; + font-size: $font-size-base; + color: $color-text-dark; + background-color: $color-input-bg; + border: 1px solid $color-input-border; + border-radius: $radius-sm; + box-shadow: $shadow-input; + transition: + border-color $transition-fast, + box-shadow $transition-fast; + appearance: none; + -webkit-appearance: none; + + &::placeholder { + color: $color-text-light; + } + + &:focus { + outline: none; + border-color: $color-primary; + box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.15); + } +} + +// ─── Input Sizes ───────────────────────────────────────────────────────────── +.input--sm { + width: 80px; + text-align: center; + font-weight: $font-weight-medium; + letter-spacing: 0.02em; +} + +// ─── Select (with arrow) ───────────────────────────────────────────────────── +.select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7' viewBox='0 0 12 7'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%237a8a9a' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right $space-3 center; + padding-right: $space-8; + cursor: pointer; + + &:hover { + border-color: $color-primary-light; + } +} + +// ─── Select Label Tag (wie "Dogument", "Verrechenbar") ─────────────────────── +.select-hint { + font-size: $font-size-xs; + color: $color-text-muted; + font-style: italic; +} + +// ─── Textarea ──────────────────────────────────────────────────────────────── +.textarea { + resize: vertical; + min-height: 72px; +} + +// ─── Help Icon ─────────────────────────────────────────────────────────────── +.input-help { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid $color-border; + background: $color-white; + color: $color-text-muted; + font-size: $font-size-xs; + font-weight: $font-weight-bold; + cursor: help; + flex-shrink: 0; + transition: border-color $transition-fast, color $transition-fast; + + &:hover { + border-color: $color-primary; + color: $color-primary; + } +} diff --git a/httpdocs/assets/styles/atoms/_typography.scss b/httpdocs/assets/styles/atoms/_typography.scss new file mode 100644 index 0000000..eb077a6 --- /dev/null +++ b/httpdocs/assets/styles/atoms/_typography.scss @@ -0,0 +1,37 @@ +@use 'variables' as *; + +// ─── Base Typography ───────────────────────────────────────────────────────── +body { + font-family: $font-family-base; + font-size: $font-size-base; + font-weight: $font-weight-regular; + line-height: $line-height-base; + color: $color-text-base; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// ─── Utility Classes ───────────────────────────────────────────────────────── +.text-xs { font-size: $font-size-xs; } +.text-sm { font-size: $font-size-sm; } +.text-base { font-size: $font-size-base; } +.text-md { font-size: $font-size-md; } +.text-lg { font-size: $font-size-lg; } +.text-xl { font-size: $font-size-xl; } + +.text-muted { color: $color-text-muted; } +.text-light { color: $color-text-light; } +.text-dark { color: $color-text-dark; } +.text-white { color: $color-white; } + +.font-medium { font-weight: $font-weight-medium; } +.font-bold { font-weight: $font-weight-bold; } + +.label { + display: block; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-text-muted; + margin-bottom: $space-2; + letter-spacing: 0.01em; +} diff --git a/httpdocs/assets/styles/atoms/_variables.scss b/httpdocs/assets/styles/atoms/_variables.scss new file mode 100644 index 0000000..1bcb227 --- /dev/null +++ b/httpdocs/assets/styles/atoms/_variables.scss @@ -0,0 +1,75 @@ +// ─── Color Palette ─────────────────────────────────────────────────────────── +$color-primary: #4a90d9; +$color-primary-dark: #3178b8; +$color-primary-light: #6aaee8; +$color-header-from: #5b9fd6; +$color-header-to: #3a7bbf; + +$color-accent: #f0a500; +$color-accent-dark: #d4900a; +$color-accent-light: #f5bc3a; + +$color-white: #ffffff; +$color-bg: #dce9f5; +$color-card: #f0f0f0; +$color-card-white: #ffffff; + +$color-text-dark: #1a2a3a; +$color-text-base: #3a4a5a; +$color-text-muted: #7a8a9a; +$color-text-light: #aab8c6; + +$color-border: #d0d8e0; +$color-input-bg: #ffffff; +$color-input-border: #b8c4d0; + +$color-day-active-bg: #1a2a3a; +$color-day-active-text:#ffffff; +$color-day-hover: rgba(255,255,255,0.2); + +// ─── Typography ────────────────────────────────────────────────────────────── +$font-family-base: 'DM Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +$font-size-xs: 0.7rem; +$font-size-sm: 0.8rem; +$font-size-base: 0.875rem; +$font-size-md: 1rem; +$font-size-lg: 1.15rem; +$font-size-xl: 1.4rem; + +$font-weight-regular: 400; +$font-weight-medium: 500; +$font-weight-bold: 700; + +$line-height-base: 1.5; + +// ─── Spacing ───────────────────────────────────────────────────────────────── +$space-1: 0.25rem; +$space-2: 0.5rem; +$space-3: 0.75rem; +$space-4: 1rem; +$space-5: 1.25rem; +$space-6: 1.5rem; +$space-8: 2rem; +$space-10: 2.5rem; +$space-12: 3rem; + +// ─── Border Radius ─────────────────────────────────────────────────────────── +$radius-sm: 4px; +$radius-md: 8px; +$radius-lg: 16px; +$radius-xl: 24px; +$radius-pill: 100px; + +// ─── Shadows ───────────────────────────────────────────────────────────────── +$shadow-card: 0 2px 12px rgba(0, 60, 120, 0.08); +$shadow-input: 0 1px 3px rgba(0, 40, 80, 0.06) inset; +$shadow-button: 0 2px 8px rgba(240, 165, 0, 0.35); + +// ─── Transitions ───────────────────────────────────────────────────────────── +$transition-fast: 0.15s ease; +$transition-base: 0.2s ease; +$transition-slow: 0.3s ease; + +// ─── Layout ────────────────────────────────────────────────────────────────── +$header-height: 88px; +$content-max-width: 860px; diff --git a/httpdocs/assets/styles/components/_account.scss b/httpdocs/assets/styles/components/_account.scss new file mode 100644 index 0000000..9fdbfa3 --- /dev/null +++ b/httpdocs/assets/styles/components/_account.scss @@ -0,0 +1,156 @@ +@use '../atoms/variables' as *; + +// ─── Page ───────────────────────────────────────────────────────────────────── +.account-page { + min-height: 100vh; + background: $color-bg; + display: flex; + flex-direction: column; +} + +// ─── Header ────────────────────────────────────────────────────────────────── +.account-header { + background: linear-gradient(135deg, $color-header-from 0%, $color-header-to 100%); + padding: $space-6; + display: flex; + align-items: center; + justify-content: space-between; + gap: $space-6; + box-shadow: 0 2px 16px rgba(0, 50, 120, 0.2); +} + +.account-header__title { + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-white; +} + +// ─── Tab-Navigation (Pill im Header) ───────────────────────────────────────── +.account-tabs { + display: flex; + background: rgba(255, 255, 255, 0.18); + border-radius: $radius-pill; + padding: 3px; + gap: $space-1; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} + +.account-tab { + display: inline-flex; + align-items: center; + padding: $space-2 $space-5; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + border-radius: $radius-pill; + transition: background $transition-fast, color $transition-fast; + white-space: nowrap; + + &:hover:not(.account-tab--active) { + color: $color-white; + background: rgba(255, 255, 255, 0.12); + } + + &--active { + color: $color-text-dark; + background: $color-white; + font-weight: $font-weight-bold; + } +} + +// ─── Content ───────────────────────────────────────────────────────────────── +.account-content { + flex: 1; + max-width: 680px; + width: 100%; + margin: $space-8 auto; + padding: 0 $space-6; +} + +// ─── Karte ─────────────────────────────────────────────────────────────────── +.account-card { + background: $color-card-white; + border-radius: $radius-lg; + box-shadow: $shadow-card; + padding: $space-8; +} + +// ─── Formular-Grid ─────────────────────────────────────────────────────────── +.account-form__grid { + display: grid; + grid-template-columns: 160px 1fr; + gap: $space-4 $space-6; + align-items: start; +} + +.account-form__label { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-text-muted; + padding-top: 7px; +} + +.account-form__field { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.account-form__hint { + font-size: $font-size-xs; + color: $color-text-muted; +} + +.account-form__link { + font-size: $font-size-sm; + color: $color-primary; + text-decoration: none; + + &:hover { text-decoration: underline; } +} + +// ─── Passwort-Sektion (toggle) ──────────────────────────────────────────────── +.account-form__pw-section { + display: contents; // bleibt im Grid-Fluss + + &[hidden] { + display: none !important; + } +} + +// ─── Actions ───────────────────────────────────────────────────────────────── +.account-form__actions { + grid-column: 1 / -1; + display: flex; + align-items: center; + gap: $space-4; + margin-top: $space-2; + padding-top: $space-4; + border-top: 1px solid $color-border; +} + +// ─── Toast ─────────────────────────────────────────────────────────────────── +.account-toast { + position: fixed; + bottom: $space-6; + right: $space-6; + background: $color-text-dark; + color: $color-white; + padding: $space-3 $space-5; + border-radius: $radius-md; + font-size: $font-size-sm; + opacity: 0; + transform: translateY(8px); + transition: opacity $transition-base, transform $transition-base; + pointer-events: none; + z-index: 9999; + + &--visible { + opacity: 1; + transform: translateY(0); + } + + &--error { background: #c83232; } +} \ No newline at end of file diff --git a/httpdocs/assets/styles/components/_crud.scss b/httpdocs/assets/styles/components/_crud.scss new file mode 100644 index 0000000..f2a68f0 --- /dev/null +++ b/httpdocs/assets/styles/components/_crud.scss @@ -0,0 +1,208 @@ +@use '../atoms/variables' as *; + +// ─── CRUD Seiten Layout ──────────────────────────────────────────────────────── +.crud-page { + max-width: $content-max-width; + margin: 0 auto; + padding: $space-6; +} + +.crud-page__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: $space-6; +} + +.crud-page__title { + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-text-dark; +} + +// ─── Liste ───────────────────────────────────────────────────────────────────── +.crud-list { + background: $color-card-white; + border-radius: $radius-lg; + box-shadow: $shadow-card; + overflow: hidden; +} + +// ─── Zeile ───────────────────────────────────────────────────────────────────── +.crud-row { + border-bottom: 1px solid rgba($color-border, 0.5); + transition: opacity 0.28s ease, transform 0.28s ease; + + &:last-child { border-bottom: none; } + + &--removing { + opacity: 0; + transform: translateX(12px); + } + + &--new { + opacity: 0; + transform: translateY(-4px); + } +} + +.crud-row__display { + display: flex; + align-items: center; + gap: $space-4; + padding: $space-4 $space-6; + transition: background $transition-fast; + + &:hover { + background: rgba($color-primary, 0.03); + + .crud-row__btn { opacity: 1; } + } +} + +.crud-row__info { + flex: 1; + min-width: 0; +} + +.crud-row__name { + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-text-dark; +} + +.crud-row__meta { + font-size: $font-size-sm; + color: $color-text-muted; + margin-left: $space-2; + font-weight: $font-weight-regular; +} + +.crud-row__actions { + display: flex; + align-items: center; + gap: $space-2; + flex-shrink: 0; +} + +.crud-row__btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + border: none; + cursor: pointer; + opacity: 0; + transition: opacity $transition-fast, background $transition-fast, color $transition-fast; + color: $color-text-muted; + + svg { width: 14px; height: 14px; pointer-events: none; } + + &--edit:hover { background: rgba($color-primary, 0.1); color: $color-primary; } + &--delete:hover{ background: rgba(200, 50, 50, 0.1); color: #c83232; } + + @media (hover: none) { opacity: 1; } +} + +// ─── Edit-Formular innerhalb der Zeile ───────────────────────────────────────── +.crud-row__edit { + padding: $space-4 $space-6; + background: rgba($color-primary, 0.03); + border-top: 1px solid rgba($color-border, 0.5); +} + +.crud-row__display[hidden] { display: none !important; } + +// ─── Create-Formular oben ────────────────────────────────────────────────────── +.crud-create { + background: $color-card; + border-radius: $radius-lg; + padding: $space-5 $space-6; + margin-bottom: $space-4; + box-shadow: $shadow-card; + display: none; + + &--visible { display: block; } +} + +// ─── Tabs (Aktiv / Archiviert) ───────────────────────────────────────────────── +.crud-tabs { + display: inline-flex; + background: $color-card-white; + border-radius: $radius-pill; + padding: 3px; + margin-bottom: $space-4; + box-shadow: $shadow-card; +} + +.crud-tab { + padding: $space-1 $space-5; + border: none; + background: transparent; + border-radius: $radius-pill; + font-family: $font-family-base; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-text-muted; + cursor: pointer; + transition: background $transition-fast, color $transition-fast; + + &:hover { color: $color-text-dark; } + + &--active { + background: $color-primary; + color: $color-white; + } +} + +// ─── Archivierte Zeile ───────────────────────────────────────────────────────── +.crud-row--archived { + .crud-row__name { + color: $color-text-muted; + text-decoration: line-through; + text-decoration-color: rgba($color-text-muted, 0.5); + } +} + +.crud-row__btn--restore { + &:hover { background: rgba(74, 180, 74, 0.12); color: #3a9a3a; } +} + +// ─── Gruppen-Header (z.B. Verrechenbar / Nicht-verrechenbar) ────────────────── +.crud-list__group { + & + & { + border-top: 2px solid $color-border; + } +} + +.crud-list__group-label { + padding: $space-3 $space-6; + font-size: $font-size-xs; + font-weight: $font-weight-bold; + color: $color-text-muted; + text-transform: uppercase; + letter-spacing: 0.06em; + background: rgba($color-primary, 0.03); + border-bottom: 1px solid rgba($color-border, 0.5); +} + +// ─── Checkbox-Label (Verrechenbar-Feld) ──────────────────────────────────────── +.crud-checkbox-label { + display: flex; + align-items: center; + gap: $space-2; + cursor: pointer; + font-size: $font-size-base; + color: $color-text-base; + + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + flex-shrink: 0; + accent-color: $color-primary; + } +} diff --git a/httpdocs/assets/styles/components/_duration-help.scss b/httpdocs/assets/styles/components/_duration-help.scss new file mode 100644 index 0000000..90d334e --- /dev/null +++ b/httpdocs/assets/styles/components/_duration-help.scss @@ -0,0 +1,56 @@ +@use '../atoms/variables' as *; + +// ── Dauer-Hilfe Tooltip ─────────────────────────────────────────────────────── +// Zeigt "?" an, auf Hover klappt der Hilfetext aus + +.duration-help { + display: inline-flex; + align-items: center; + cursor: help; + flex-shrink: 0; + + &__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid $color-border; + background: $color-white; + color: $color-text-muted; + font-size: $font-size-xs; + font-weight: $font-weight-bold; + flex-shrink: 0; + transition: + width $transition-base, + opacity $transition-base, + border $transition-base, + margin $transition-base; + overflow: hidden; + } + + &__hint { + font-size: $font-size-xs; + color: $color-text-muted; + white-space: nowrap; + max-width: 0; + overflow: hidden; + opacity: 0; + transition: + max-width 0.28s ease, + opacity 0.2s ease; + } + + &:hover &__icon { + width: 0; + opacity: 0; + border-width: 0; + margin-right: 0; + } + + &:hover &__hint { + max-width: 500px; + opacity: 1; + } +} diff --git a/httpdocs/assets/styles/components/_entry-form.scss b/httpdocs/assets/styles/components/_entry-form.scss new file mode 100644 index 0000000..04859b3 --- /dev/null +++ b/httpdocs/assets/styles/components/_entry-form.scss @@ -0,0 +1,50 @@ +@use '../atoms/variables' as *; + +// ─── Entry Form Card ───────────────────────────────────────────────────────── +.entry-form { + background: $color-card; + border-radius: $radius-lg; + padding: $space-6 $space-8; + box-shadow: $shadow-card; +} + +.entry-form__grid { + display: grid; + grid-template-columns: 120px 1fr; + gap: $space-4 $space-6; + align-items: center; +} + +.entry-form__label { + font-size: $font-size-base; + font-weight: $font-weight-regular; + color: $color-text-base; + text-align: right; + padding-right: $space-2; + white-space: nowrap; +} + +.entry-form__field { + display: flex; + align-items: center; + gap: $space-2; +} + +.entry-form__field--selects { + display: flex; + gap: $space-3; + flex-wrap: wrap; + + .select { + flex: 1; + min-width: 180px; + } +} + +.entry-form__actions { + grid-column: 2; + display: flex; + align-items: center; + gap: $space-4; + padding-top: $space-2; +} diff --git a/httpdocs/assets/styles/components/_entry-list.scss b/httpdocs/assets/styles/components/_entry-list.scss new file mode 100644 index 0000000..f1bacbb --- /dev/null +++ b/httpdocs/assets/styles/components/_entry-list.scss @@ -0,0 +1,170 @@ +@use '../atoms/variables' as *; + +// ─── Entry List Container ────────────────────────────────────────────────── +.entry-list { + background: $color-card-white; + border-radius: $radius-lg; + box-shadow: $shadow-card; + overflow: hidden; + transition: opacity 0.18s ease; + + &--fading { opacity: 0; } +} + +// ─── Empty State ────────────────────────────────────────────────────────── +.empty-state { + background: $color-card-white; + border-radius: $radius-lg; + padding: $space-6 $space-8; + box-shadow: $shadow-card; +} + +.empty-state__title { + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-text-dark; + margin: 0 0 $space-2; +} + +// ─── Footer mit Gesamtdauer ─────────────────────────────────────────────── +.entry-list__footer { + display: flex; + justify-content: flex-end; + // 2 Buttons (28px) + 2× gap (8px) + eigener padding = Badge bündig + padding: $space-3 calc(#{$space-8} + 28px + 28px + #{$space-2} + #{$space-2}); + border-top: 1px solid $color-border; +} + +.entry-list__total { + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-text-dark; + background: $color-card; + border-radius: $radius-pill; + padding: $space-1 $space-4; +} + +// ─── Entry Row ──────────────────────────────────────────────────────────── +.entry-row { + border-bottom: 1px solid rgba($color-border, 0.5); + transition: opacity 0.28s ease, transform 0.28s ease, max-height 0.28s ease; + + &:last-child { border-bottom: none; } + + // Fade-in bei neuem Eintrag + &--new { + opacity: 0; + transform: translateY(-6px); + } + + // Fade-out beim Löschen + &--removing { + opacity: 0; + transform: translateX(12px); + max-height: 0; + overflow: hidden; + padding: 0; + margin: 0; + } +} + +// ─── Anzeige-Modus ──────────────────────────────────────────────────────── +.entry-row__display { + display: flex; + align-items: center; + justify-content: space-between; + gap: $space-4; + padding: $space-4 $space-8; + + &:hover { + background: rgba($color-primary, 0.03); + + .entry-row__btn { + opacity: 1; + } + } + + &[hidden] { + display: none !important; + } +} + +.entry-row__info { + flex: 1; + min-width: 0; +} + +.entry-row__title { + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-text-dark; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.entry-row__note { + font-size: $font-size-sm; + color: $color-text-muted; + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.entry-row__actions { + display: flex; + align-items: center; + gap: $space-2; + flex-shrink: 0; +} + +.entry-row__badge { + font-size: $font-size-sm; + font-weight: $font-weight-bold; + color: $color-text-dark; + background: $color-card; + border-radius: $radius-pill; + padding: $space-1 $space-3; + min-width: 48px; + text-align: center; + font-variant-numeric: tabular-nums; +} + +.entry-row__btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + border: none; + cursor: pointer; + opacity: 0; + transition: opacity $transition-fast, background $transition-fast, color $transition-fast; + color: $color-text-muted; + + svg { width: 14px; height: 14px; pointer-events: none; } + + &--edit:hover { background: rgba($color-primary, 0.1); color: $color-primary; } + &--delete:hover{ background: rgba(200, 50, 50, 0.1); color: #c83232; } + + // immer sichtbar auf Touch-Geräten + @media (hover: none) { opacity: 1; } +} + +// ─── Bearbeiten-Modus ───────────────────────────────────────────────────── +.entry-row__edit { + padding: $space-4 $space-8; + background: rgba($color-primary, 0.03); + border-top: 1px solid rgba($color-border, 0.5); +} + +.entry-form__grid--inline { + // Gleiche Grid-Struktur wie das Haupt-Formular + display: grid; + grid-template-columns: 130px 1fr; + gap: $space-3 $space-6; + align-items: center; +} diff --git a/httpdocs/assets/styles/components/_greeting.scss b/httpdocs/assets/styles/components/_greeting.scss new file mode 100644 index 0000000..5d7b49e --- /dev/null +++ b/httpdocs/assets/styles/components/_greeting.scss @@ -0,0 +1,15 @@ +@use '../atoms/variables' as *; + +// ─── Begrüßung zwischen Header und Formular ─────────────────────────────────── +.greeting { + max-width: $content-max-width; + width: 100%; + margin: 0 auto; + padding: $space-5 $space-6 0; + + &__text { + font-size: $font-size-lg; + font-weight: $font-weight-bold; + color: $color-text-dark; + } +} diff --git a/httpdocs/assets/styles/components/_login.scss b/httpdocs/assets/styles/components/_login.scss new file mode 100644 index 0000000..fc8592f --- /dev/null +++ b/httpdocs/assets/styles/components/_login.scss @@ -0,0 +1,98 @@ +@use '../atoms/variables' as *; + +// ─── Login Page ─────────────────────────────────────────────────────────────── +.login-body { + min-height: 100vh; + background: $color-bg; + display: flex; + align-items: center; + justify-content: center; +} + +// ─── Card ───────────────────────────────────────────────────────────────────── +.login-card { + background: $color-card-white; + border-radius: $radius-xl; + padding: $space-10 $space-12; + width: 100%; + max-width: 540px; + box-shadow: $shadow-card; +} + +.login-card__title { + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-text-dark; + text-align: center; + margin-bottom: $space-8; +} + +.login-card__error { + background: rgba(200, 50, 50, 0.08); + border: 1px solid rgba(200, 50, 50, 0.25); + border-radius: $radius-sm; + color: #c83232; + font-size: $font-size-sm; + padding: $space-3 $space-4; + margin-bottom: $space-6; +} + +// ─── Form Grid ──────────────────────────────────────────────────────────────── +.login-form__grid { + display: grid; + grid-template-columns: 90px 1fr; + gap: $space-5 $space-4; + align-items: center; + margin-bottom: $space-5; +} + +.login-form__label { + font-size: $font-size-base; + color: $color-text-muted; + text-align: right; + padding-right: $space-2; +} + +.login-form__field { + display: flex; + align-items: center; + gap: $space-3; +} + +.login-form__field--password { + // Platz für "vergessen?" Link, falls später hinzukommt +} + +// ─── "Angemeldet bleiben" ───────────────────────────────────────────────────── +.login-form__remember { + display: flex; + justify-content: center; + margin-bottom: $space-6; +} + +.login-form__remember-label { + display: flex; + align-items: center; + gap: $space-2; + cursor: pointer; + font-size: $font-size-base; + color: $color-text-base; + + input[type='checkbox'] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: $color-primary; + } +} + +// ─── Submit ─────────────────────────────────────────────────────────────────── +.login-form__actions { + display: flex; + justify-content: center; +} + +.login-form__submit { + padding: $space-3 $space-10; + font-size: $font-size-md; +} \ No newline at end of file diff --git a/httpdocs/assets/styles/components/_main-nav.scss b/httpdocs/assets/styles/components/_main-nav.scss new file mode 100644 index 0000000..cc662da --- /dev/null +++ b/httpdocs/assets/styles/components/_main-nav.scss @@ -0,0 +1,50 @@ +@use '../atoms/variables' as *; + +// ─── Dunkle Top-Navigation ──────────────────────────────────────────────────── +.main-nav { + background: #1a2a3a; + display: flex; + align-items: stretch; + justify-content: space-between; + padding: 0 $space-4; + height: 44px; + flex-shrink: 0; + position: sticky; + top: 0; + z-index: 200; +} + +.main-nav__left, +.main-nav__right { + display: flex; + align-items: stretch; + gap: 0; +} + +.main-nav__item { + display: inline-flex; + align-items: center; + padding: 0 $space-4; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: rgba(255, 255, 255, 0.65); + text-decoration: none; + border-bottom: 2px solid transparent; + transition: color $transition-fast, border-color $transition-fast; + white-space: nowrap; + + &:hover { + color: $color-white; + } + + &--active { + color: $color-white; + border-bottom-color: $color-primary-light; + } + + &--disabled { + opacity: 0.35; + pointer-events: none; + cursor: default; + } +} diff --git a/httpdocs/assets/styles/components/_month-calendar.scss b/httpdocs/assets/styles/components/_month-calendar.scss new file mode 100644 index 0000000..bf6305b --- /dev/null +++ b/httpdocs/assets/styles/components/_month-calendar.scss @@ -0,0 +1,137 @@ +@use '../atoms/variables' as *; + +// ─── Monatskalender Container ───────────────────────────────────────────────── +.month-calendar { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 380px; + background: linear-gradient(160deg, $color-primary-light, $color-primary-dark); + border-radius: $radius-xl; + padding: $space-4; + box-shadow: 0 8px 32px rgba(0, 60, 120, 0.35); + z-index: 200; + transform-origin: top right; + transition: + opacity 0.28s ease, + transform 0.28s ease; + + &--hidden { + opacity: 0; + transform: scaleY(0.92) translateY(-8px); + pointer-events: none; + } + + &--visible { + opacity: 1; + transform: scaleY(1) translateY(0); + } +} + +// ─── Header (Monat/Jahr + Navigation) ──────────────────────────────────────── +.month-calendar__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: $space-4; +} + +.month-calendar__title { + font-size: $font-size-md; + font-weight: $font-weight-bold; + color: $color-white; + letter-spacing: 0.01em; +} + +.month-calendar__arrow { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + border: none; + color: $color-white; + cursor: pointer; + transition: background $transition-fast; + + &:hover { background: rgba(255, 255, 255, 0.2); } + + svg { width: 8px; height: 14px; } +} + +.month-calendar__close { + // erbt .week-nav__cal Styles – hier nur Positionierung + margin-left: 0; +} + +// ─── Grid ───────────────────────────────────────────────────────────────────── +.month-calendar__grid { + &.slide-out-left { animation: slideOutLeft 0.16s ease forwards; } + &.slide-out-right { animation: slideOutRight 0.16s ease forwards; } +} + +.month-calendar__weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + margin-bottom: $space-2; + + span { + text-align: center; + font-size: $font-size-xs; + font-weight: $font-weight-bold; + color: rgba(255, 255, 255, 0.6); + padding: $space-1 0; + text-transform: uppercase; + letter-spacing: 0.04em; + } +} + +.month-calendar__days { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} + +// ─── Einzelner Tag ─────────────────────────────────────────────────────────── +.month-day { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-white; + cursor: pointer; + transition: background $transition-fast; + user-select: none; + + &:hover:not(&--other) { + background: rgba(255, 255, 255, 0.2); + } + + // Tage aus Vor-/Nachmonat + &--other { + color: rgba(255, 255, 255, 0.35); + cursor: default; + } + + // Heutiger Tag + &--today { + font-weight: $font-weight-bold; + background: $color-white; + color: $color-text-dark; + + &:hover { + background: rgba(255, 255, 255, 0.9); + } + } + + // Ausgewählter Tag + &--active:not(&--today) { + background: rgba(255, 255, 255, 0.25); + font-weight: $font-weight-bold; + } +} diff --git a/httpdocs/assets/styles/components/_register.scss b/httpdocs/assets/styles/components/_register.scss new file mode 100644 index 0000000..2eca36a --- /dev/null +++ b/httpdocs/assets/styles/components/_register.scss @@ -0,0 +1,193 @@ +@use '../atoms/variables' as *; + +.register-body { + min-height: 100vh; + background: $color-bg; + display: flex; + align-items: flex-start; + justify-content: center; + padding: $space-10 $space-4; +} + +// ─── Wrapper ────────────────────────────────────────────────────────────────── +.register-page { + width: 100%; + max-width: 580px; +} + +.register-card { + background: $color-card-white; + border-radius: $radius-xl; + padding: $space-10 $space-12; + box-shadow: $shadow-card; +} + +.register-card__brand { + text-align: center; + margin-bottom: $space-6; + + a { + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-primary; + text-decoration: none; + letter-spacing: 0.02em; + } +} + +.register-card__title { + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-text-dark; + text-align: center; + margin-bottom: $space-2; +} + +.register-card__sub { + font-size: $font-size-sm; + color: $color-text-muted; + text-align: center; + margin-bottom: $space-8; +} + +// ─── Errors ─────────────────────────────────────────────────────────────────── +.register-errors { + &:not(:empty) { + background: rgba(200, 50, 50, 0.07); + border: 1px solid rgba(200, 50, 50, 0.25); + border-radius: $radius-sm; + color: #c83232; + font-size: $font-size-sm; + padding: $space-3 $space-4; + margin-bottom: $space-6; + + p { margin: 0; line-height: 1.6; } + p + p { margin-top: $space-1; } + } +} + +// ─── Fieldsets ──────────────────────────────────────────────────────────────── +.register-fieldset { + border: none; + padding: 0; + margin: 0 0 $space-8; +} + +.register-fieldset__legend { + font-size: $font-size-sm; + font-weight: $font-weight-bold; + color: $color-text-muted; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: $space-5; + display: block; +} + +// ─── Einzelnes Feld ─────────────────────────────────────────────────────────── +.register-field { + margin-bottom: $space-5; + + &:last-child { margin-bottom: 0; } +} + +.register-field__label { + display: block; + font-size: $font-size-sm; + color: $color-text-muted; + margin-bottom: $space-2; +} + +.register-field__hint { + margin-top: $space-2; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.register-field__slug { + color: $color-primary; + font-family: monospace; + font-size: $font-size-sm; +} + +// ─── Zweispaltig ────────────────────────────────────────────────────────────── +.register-field-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $space-4; + margin-bottom: $space-5; +} + +// ─── Actions ────────────────────────────────────────────────────────────────── +.register-actions { + margin-top: $space-8; +} + +.register-actions__submit { + width: 100%; + padding: $space-4; + font-size: $font-size-md; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.register-actions__login { + text-align: center; + margin-top: $space-4; + font-size: $font-size-sm; + color: $color-text-muted; + + a { + color: $color-primary; + text-decoration: none; + + &:hover { text-decoration: underline; } + } +} + +// ─── Erfolgs-State ──────────────────────────────────────────────────────────── +.register-success { + text-align: center; + padding: $space-6 0; +} + +.register-success__icon { + width: 56px; + height: 56px; + border-radius: 50%; + background: #e6f5ee; + color: #2d9e60; + font-size: 1.6rem; + font-weight: $font-weight-bold; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto $space-6; + + &--error { + background: rgba(200, 50, 50, 0.08); + color: #c83232; + } +} + +.register-success__title { + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-text-dark; + margin-bottom: $space-4; +} + +.register-success__text { + font-size: $font-size-md; + color: $color-text-base; + line-height: $line-height-base; + margin-bottom: $space-3; +} + +.register-success__hint { + font-size: $font-size-sm; + color: $color-text-muted; + line-height: $line-height-base; +} \ No newline at end of file diff --git a/httpdocs/assets/styles/components/_team.scss b/httpdocs/assets/styles/components/_team.scss new file mode 100644 index 0000000..5f11c91 --- /dev/null +++ b/httpdocs/assets/styles/components/_team.scss @@ -0,0 +1,164 @@ +@use '../atoms/variables' as *; + +// ─── Ausstehend-Badge ────────────────────────────────────────────────────────── +.team-badge { + display: inline-block; + padding: 1px 8px; + border-radius: $radius-pill; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + margin-left: $space-2; + + &--pending { + background: rgba(232, 130, 10, 0.12); + color: #b86200; + } +} + +// ─── Modal ───────────────────────────────────────────────────────────────────── +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + + &[hidden] { display: none !important; } +} + +.modal-card { + background: $color-card-white; + border-radius: $radius-lg; + box-shadow: $shadow-card; + width: 100%; + max-width: 460px; + padding: 0; + overflow: hidden; +} + +.modal-card__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: $space-5 $space-6; + border-bottom: 1px solid rgba($color-border, 0.6); +} + +.modal-card__title { + font-size: $font-size-lg; + font-weight: $font-weight-bold; + color: $color-text-dark; +} + +.modal-card__close { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: transparent; + border: none; + cursor: pointer; + color: $color-text-muted; + border-radius: 50%; + transition: background $transition-fast; + svg { width: 16px; height: 16px; } + &:hover { background: rgba($color-border, 0.5); } +} + +.modal-card__body { + padding: $space-5 $space-6; + display: flex; + flex-direction: column; + gap: $space-4; +} + +.modal-card__footer { + display: flex; + justify-content: flex-end; + gap: $space-3; + padding: $space-4 $space-6; + border-top: 1px solid rgba($color-border, 0.6); +} + +// ─── Formularfelder im Modal ─────────────────────────────────────────────────── +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $space-4; +} + +.form-field { + display: flex; + flex-direction: column; + gap: $space-1; +} + +.form-field__label { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-text-dark; +} + +.form-errors { + margin: 0 $space-6; + padding: $space-3 $space-4; + background: rgba(200, 50, 50, 0.08); + border-radius: $radius-md; + border: 1px solid rgba(200, 50, 50, 0.2); + color: #c83232; + font-size: $font-size-sm; + + &[hidden] { display: none !important; } + + ul { margin: 0; padding-left: 1.2em; } +} + +// ─── Empty State ─────────────────────────────────────────────────────────────── +.crud-list__empty { + padding: $space-6; + text-align: center; + color: $color-text-muted; + font-size: $font-size-sm; +} + +// ─── Rollen-Selector (Inline-Edit + Modal) ──────────────────────────────────── +.team-role-selector { + display: flex; + flex-direction: row; + gap: $space-5; + flex-wrap: wrap; + + &--disabled { + opacity: 0.45; + pointer-events: none; + } +} + +.team-role-option { + display: flex; + align-items: center; + gap: $space-2; + cursor: pointer; + + input[type='radio'] { + accent-color: $color-primary; + width: 15px; + height: 15px; + flex-shrink: 0; + cursor: pointer; + } + + &__label { + font-size: $font-size-sm; + color: $color-text-dark; + } +} + +.team-role-hint { + margin-top: $space-1; + font-size: $font-size-xs; + color: $color-text-muted; +} \ No newline at end of file diff --git a/httpdocs/assets/styles/components/_week-nav.scss b/httpdocs/assets/styles/components/_week-nav.scss new file mode 100644 index 0000000..4e3ebb9 --- /dev/null +++ b/httpdocs/assets/styles/components/_week-nav.scss @@ -0,0 +1,122 @@ +@use '../atoms/variables' as *; + +// ─── Wrapper ───────────────────────────────────────────────────────────────── +.week-nav { + display: flex; + align-items: center; + gap: $space-1; + background: rgba(255, 255, 255, 0.18); + border-radius: $radius-pill; + padding: $space-1 $space-2; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} + +// ─── Pfeile ────────────────────────────────────────────────────────────────── +.week-nav__arrow { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + border: none; + color: $color-white; + cursor: pointer; + text-decoration: none; + flex-shrink: 0; + transition: background $transition-fast; + + &:hover { background: rgba(255, 255, 255, 0.2); } + + svg { width: 8px; height: 14px; } +} + +// ─── Tage-Container (Slide-Animation) ──────────────────────────────────────── +.week-nav__days { + display: flex; + align-items: center; + gap: $space-1; + overflow: hidden; + position: relative; + + &.slide-out-left { animation: slideOutLeft 0.18s ease forwards; } + &.slide-out-right { animation: slideOutRight 0.18s ease forwards; } + &.slide-in-right { animation: slideInRight 0.18s ease forwards; } + &.slide-in-left { animation: slideInLeft 0.18s ease forwards; } +} + +@keyframes slideOutLeft { to { opacity: 0; transform: translateX(-24px); } } +@keyframes slideOutRight { to { opacity: 0; transform: translateX(24px); } } +@keyframes slideInRight { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: translateX(0); } } +@keyframes slideInLeft { from { opacity: 0; transform: translateX(-24px); } to { opacity: 1; transform: translateX(0); } } + +// ─── Einzelner Tag ─────────────────────────────────────────────────────────── +.week-nav__day { + display: flex; + flex-direction: column; + align-items: center; + padding: $space-2 $space-3; + border-radius: $radius-md; + cursor: pointer; + text-decoration: none; + width: 64px; + flex-shrink: 0; + flex-grow: 0; + transition: background $transition-fast; + user-select: none; + + &:hover:not(.week-nav__day--active) { + background: rgba(255, 255, 255, 0.15); + } + + &--active { + background: $color-white; + + .week-nav__day-name, + .week-nav__day-date { color: $color-text-dark; } + } + + &--today:not(&--active) { + .week-nav__day-name { + text-decoration: underline; + text-underline-offset: 3px; + } + } +} + +.week-nav__day-name { + font-size: $font-size-sm; + font-weight: $font-weight-bold; + color: $color-white; + line-height: 1.2; +} + +.week-nav__day-date { + font-size: $font-size-xs; + color: rgba(255, 255, 255, 0.78); + line-height: 1.3; +} + +// ─── Kalender-Icon ─────────────────────────────────────────────────────────── +.week-nav__cal { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: $radius-md; + background: rgba(255, 255, 255, 0.2); + color: $color-white; + cursor: pointer; + border: none; + margin-left: $space-1; + flex-shrink: 0; + transition: background $transition-fast; + + svg { width: 16px; height: 16px; pointer-events: none; } + + &:hover, + &--active { background: rgba(255, 255, 255, 0.35); } +} diff --git a/httpdocs/assets/styles/main.scss b/httpdocs/assets/styles/main.scss new file mode 100644 index 0000000..478f9bf --- /dev/null +++ b/httpdocs/assets/styles/main.scss @@ -0,0 +1,42 @@ +// ─── Atoms ──────────────────────────────────────────────────────────────────── +@use 'atoms/variables' as *; +@use 'atoms/typography'; +@use 'atoms/buttons'; +@use 'atoms/inputs'; + +// ─── Components ─────────────────────────────────────────────────────────────── +@use 'components/week-nav'; +@use 'components/entry-form'; +@use 'components/entry-list'; +@use 'components/month-calendar'; +@use 'components/duration-help'; +@use 'components/main-nav'; +@use 'components/greeting'; +@use 'components/crud'; +@use 'components/login'; +@use 'components/register'; +@use 'components/account'; +@use 'components/team'; + +// ─── Sections ───────────────────────────────────────────────────────────────── +@use 'sections/timetracking'; +@use 'sections/home'; + +// ─── Reset / Base ───────────────────────────────────────────────────────────── +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; +} + +body { + background: $color-bg; +} + +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap'); diff --git a/httpdocs/assets/styles/sections/_home.scss b/httpdocs/assets/styles/sections/_home.scss new file mode 100644 index 0000000..9e6a11b --- /dev/null +++ b/httpdocs/assets/styles/sections/_home.scss @@ -0,0 +1,67 @@ +@use '../atoms/variables' as *; + +.home-body { + min-height: 100vh; + background: $color-bg; + display: flex; + flex-direction: column; +} + +// ─── Header ────────────────────────────────────────────────────────────────── +.home-header { + background: linear-gradient(135deg, $color-header-from, $color-header-to); + padding: $space-4 $space-8; +} + +.home-header__inner { + max-width: 1000px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} + +.home-header__brand { + font-size: $font-size-lg; + font-weight: $font-weight-bold; + color: $color-white; + + span { + font-weight: $font-weight-regular; + opacity: 0.8; + } +} + +// ─── Hero ───────────────────────────────────────────────────────────────────── +.home-hero { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: $space-12 $space-8; +} + +.home-hero__inner { + text-align: center; + max-width: 600px; +} + +.home-hero__title { + font-size: 2.8rem; + font-weight: $font-weight-bold; + color: $color-text-dark; + line-height: 1.2; + margin-bottom: $space-6; +} + +.home-hero__sub { + font-size: $font-size-lg; + color: $color-text-muted; + margin-bottom: $space-10; + line-height: $line-height-base; +} + +.home-hero__cta { + font-size: $font-size-md; + padding: $space-4 $space-10; +} \ No newline at end of file diff --git a/httpdocs/assets/styles/sections/_timetracking.scss b/httpdocs/assets/styles/sections/_timetracking.scss new file mode 100644 index 0000000..b2070ea --- /dev/null +++ b/httpdocs/assets/styles/sections/_timetracking.scss @@ -0,0 +1,54 @@ +@use '../atoms/variables' as *; + +// ─── Page Wrapper ───────────────────────────────────────────────────────────── +.tt-page { + min-height: 100vh; + background: $color-bg; + display: flex; + flex-direction: column; +} + +// ─── Header Section ────────────────────────────────────────────────────────── +.tt-header { + background: linear-gradient(135deg, $color-header-from 0%, $color-header-to 100%); + padding: $space-4 $space-6; + display: flex; + align-items: center; + justify-content: space-between; + gap: $space-6; + min-height: $header-height; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 2px 16px rgba(0, 50, 120, 0.2); +} + +.tt-header__meta { + flex-shrink: 0; +} + +.tt-header__date { + font-size: $font-size-lg; + font-weight: $font-weight-bold; + color: $color-white; + line-height: 1.2; +} + +.tt-header__kw { + font-size: $font-size-sm; + font-weight: $font-weight-bold; + color: rgba(255, 255, 255, 0.75); + line-height: 1.3; +} + +// ─── Main Content ───────────────────────────────────────────────────────────── +.tt-content { + flex: 1; + max-width: $content-max-width; + width: 100%; + margin: 0 auto; + padding: $space-6 $space-6; + display: flex; + flex-direction: column; + gap: $space-4; +} diff --git a/httpdocs/bin/console b/httpdocs/bin/console new file mode 100755 index 0000000..d8d530e --- /dev/null +++ b/httpdocs/bin/console @@ -0,0 +1,21 @@ +#!/usr/bin/env php +=8.2", + "ext-ctype": "*", + "ext-iconv": "*", + "doctrine/doctrine-bundle": "^3.2.2", + "doctrine/doctrine-migrations-bundle": "^4.0", + "doctrine/orm": "^3.6.6", + "symfony/console": "7.4.*", + "symfony/dotenv": "7.4.*", + "symfony/flex": "^2.10", + "symfony/form": "7.4.*", + "symfony/framework-bundle": "7.4.*", + "symfony/mailer": "7.4.*", + "symfony/monolog-bundle": "^4.0.2", + "symfony/runtime": "7.4.*", + "symfony/security-bundle": "7.4.*", + "symfony/translation": "7.4.*", + "symfony/twig-bundle": "7.4.*", + "symfony/validator": "7.4.*", + "symfony/webpack-encore-bundle": "^2.4", + "symfony/yaml": "7.4.*" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "bump-after-update": true, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "7.4.*", + "docker": false + } + }, + "require-dev": { + "symfony/maker-bundle": "^1.67" + } +} diff --git a/httpdocs/composer.lock b/httpdocs/composer.lock new file mode 100644 index 0000000..e32a8ff --- /dev/null +++ b/httpdocs/composer.lock @@ -0,0 +1,6634 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "dae707f4e483331f467dcf211922216c", + "packages": [ + { + "name": "doctrine/collections", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-json": "*", + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.6.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2026-01-15T10:01:58+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.4.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "61e730f1658814821a85f2402c945f3883407dec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", + "reference": "61e730f1658814821a85f2402c945f3883407dec", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.4.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-03-20T08:52:12+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "af84173db6978c3d2688ea3bcf3a91720b0704ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/af84173db6978c3d2688ea3bcf3a91720b0704ce", + "reference": "af84173db6978c3d2688ea3bcf3a91720b0704ce", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^4.0", + "doctrine/deprecations": "^1.0", + "doctrine/persistence": "^4", + "doctrine/sql-formatter": "^1.0.1", + "php": "^8.4", + "symfony/cache": "^6.4 || ^7.0 || ^8.0", + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3 || ^8.0", + "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/service-contracts": "^3" + }, + "conflict": { + "doctrine/orm": "<3.0 || >=4.0", + "twig/twig": "<3.0.4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "doctrine/orm": "^3.4.4", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2.0", + "phpunit/phpunit": "^12.3.10", + "psr/log": "^3.0", + "symfony/doctrine-messenger": "^6.4 || ^7.0 || ^8.0", + "symfony/expression-language": "^6.4 || ^7.0 || ^8.0", + "symfony/messenger": "^6.4 || ^7.0 || ^8.0", + "symfony/property-info": "^6.4 || ^7.0 || ^8.0", + "symfony/security-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/stopwatch": "^6.4 || ^7.0 || ^8.0", + "symfony/string": "^6.4 || ^7.0 || ^8.0", + "symfony/twig-bridge": "^6.4 || ^7.0 || ^8.0", + "symfony/validator": "^6.4 || ^7.0 || ^8.0", + "symfony/web-profiler-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/yaml": "^6.4 || ^7.0 || ^8.0", + "twig/twig": "^3.21.1" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/3.2.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2025-12-24T12:24:29+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "20505da78735744fb4a42a3bb9a416b345ad6f7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/20505da78735744fb4a42a3bb9a416b345ad6f7c", + "reference": "20505da78735744fb4a42a3bb9a416b345ad6f7c", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^4", + "doctrine/doctrine-bundle": "^3", + "doctrine/migrations": "^3.2", + "php": "^8.4", + "psr/log": "^3", + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "symfony/deprecation-contracts": "^3", + "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/http-foundation": "^6.4 || ^7.0 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.0 || ^8.0", + "symfony/service-contracts": "^3.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/coding-standard": "^14", + "doctrine/orm": "^3", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2", + "phpunit/phpunit": "^12.5", + "symfony/var-exporter": "^6.4 || ^7 || ^8" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/4.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2025-12-05T08:14:38+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2026-01-29T07:11:08+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "shasum": "" + }, + "require": { + "php": "^8.4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2026-01-05T06:47:08+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.9.7", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.6 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2.0", + "php": "^8.1", + "psr/log": "^1.1.3 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.2 || ^7.0 || ^8.0" + }, + "conflict": { + "doctrine/orm": "<2.12 || >=4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", + "doctrine/sql-formatter": "^1.0", + "ext-pdo_sqlite": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2", + "phpunit/phpunit": "^10.3 || ^11.0 || ^12.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.9.7" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2026-04-23T19:33:20+00:00" + }, + { + "name": "doctrine/orm", + "version": "3.6.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "471b12949ff9bc23ecdc809ce838613c1aa9a0b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/471b12949ff9bc23ecdc809ce838613c1aa9a0b9", + "reference": "471b12949ff9bc23ecdc809ce838613c1aa9a0b9", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", + "ext-ctype": "*", + "php": "^8.1", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.1.23", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.5.0 || ^11.5", + "psr/log": "^1 || ^2 || ^3", + "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/3.6.6" + }, + "time": "2026-05-21T06:05:47+00:00" + }, + { + "name": "doctrine/persistence", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "doctrine/event-manager": "^1 || ^2", + "php": "^8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.58 || ^12", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/4.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2026-04-26T12:12:52+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.5.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ergebnis/phpunit-slow-test-detector": "^2.20", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^10.5.58" + }, + "bin": [ + "bin/sql-formatter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4" + }, + "time": "2026-02-08T16:21:46+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/asset", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", + "reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "902d621e0b6ef0ebeaa133770b5c339a19328589" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/902d621e0b6ef0ebeaa133770b5c339a19328589", + "reference": "902d621e0b6ef0ebeaa133770b5c339a19328589", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "225e8a254166bd3442e370c6f50145465db63831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", + "reference": "225e8a254166bd3442e370c6f50145465db63831", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-05T15:33:14+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/config", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1|^8.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-03T14:20:49+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/ed0107e43ab452aa77ae99e005b95e56b556e075", + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:04:42+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", + "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-06T11:55:30+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "7a87c85853f3069e3657a823c62b02952de46b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/7a87c85853f3069e3657a823c62b02952de46b0a", + "reference": "7a87c85853f3069e3657a823c62b02952de46b0a", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^2", + "doctrine/persistence": "^3.1|^4", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/collections": "<1.8", + "doctrine/dbal": "<3.6", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.15", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/form": "<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/lock": "<6.4", + "symfony/messenger": "<6.4", + "symfony/property-info": "<6.4", + "symfony/security-bundle": "<6.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<7.4" + }, + "require-dev": { + "doctrine/collections": "^1.8|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^3.6|^4", + "doctrine/orm": "^2.15|^3", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/doctrine-messenger": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.2|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-29T14:19:39+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "82e9b1355c68ef7b96397dbd34cc75a92eebae7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/82e9b1355c68ef7b96397dbd34cc75a92eebae7c", + "reference": "82e9b1355c68ef7b96397dbd34cc75a92eebae7c", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/process": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T13:02:51+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T16:38:44+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "9cd384775973eabbf6e8b05784dda279fc67c28d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/9cd384775973eabbf6e8b05784dda279fc67c28d", + "reference": "9cd384775973eabbf6e8b05784dda279fc67c28d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.1" + }, + "conflict": { + "composer/semver": "<1.7.2", + "symfony/dotenv": "<5.4" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^6.4|^7.4|^8.0", + "symfony/filesystem": "^6.4|^7.4|^8.0", + "symfony/phpunit-bridge": "^6.4|^7.4|^8.0", + "symfony/process": "^6.4|^7.4|^8.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-16T09:38:19+00:00" + }, + { + "name": "symfony/form", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "b6c107af659106abec1771d9d7d22da528644d3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/b6c107af659106abec1771d9d7d22da528644d3a", + "reference": "b6c107af659106abec1771d9d7d22da528644d3a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/options-resolver": "^7.3|^8.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/error-handler": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<7.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4.12|^7.1.5|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Form\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows to easily create, process and reuse HTML forms", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/form/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-29T14:39:19+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "637f5cac1ac2698a012b41610215bf366004295f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/637f5cac1ac2698a012b41610215bf366004295f", + "reference": "637f5cac1ac2698a012b41610215bf366004295f", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^7.4.4|^8.0.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/asset": "<6.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.4", + "symfony/console": "<6.4", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/form": "<7.4", + "symfony/http-client": "<6.4", + "symfony/lock": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<7.4", + "symfony/mime": "<6.4.37|>=7.0,<7.4.9|>=8.0,<8.0.9", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<6.4", + "symfony/security-csrf": "<7.2", + "symfony/serializer": "<7.2.5", + "symfony/stopwatch": "<6.4", + "symfony/translation": "<7.3", + "symfony/twig-bridge": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/webhook": "<7.2", + "symfony/workflow": "<7.4" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4.37|^7.4.9|^8.0.9", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.2|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", + "twig/twig": "^3.12" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:04:42+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", + "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T09:27:11+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b198dd66c211c97119bcaaff7c13431dbbb5e470", + "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "20bb2345ac7a9dd57724b6b7ada92c6d7d67b4b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/20bb2345ac7a9dd57724b6b7ada92c6d7d67b4b8", + "reference": "20bb2345ac7a9dd57724b6b7ada92c6d7d67b4b8", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "c012c6aba13129eb02aa7dd61e66e720911d8598" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/c012c6aba13129eb02aa7dd61e66e720911d8598", + "reference": "c012c6aba13129eb02aa7dd61e66e720911d8598", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "monolog/monolog": "^3.5", + "php": ">=8.2", + "symfony/config": "^7.3 || ^8.0", + "symfony/dependency-injection": "^7.3 || ^8.0", + "symfony/http-kernel": "^7.3 || ^8.0", + "symfony/monolog-bridge": "^7.3 || ^8.0", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.41 || ^12.3", + "symfony/console": "^7.3 || ^8.0", + "symfony/yaml": "^7.3 || ^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v4.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-02T18:27:21+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "18a7d92126c95962f7efbcc9e421ba710a366847" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/18a7d92126c95962f7efbcc9e421ba710a366847", + "reference": "18a7d92126c95962f7efbcc9e421ba710a366847", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:13:48+00:00" + }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "3510b63d07376b04e57e27e82607d468bb134f78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/3510b63d07376b04e57e27e82607d468bb134f78", + "reference": "3510b63d07376b04e57e27e82607d468bb134f78", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:50:15+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T18:47:49+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:10:57+00:00" + }, + { + "name": "symfony/property-access", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", + "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/runtime", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "0b032fa77359745db793df5aff626779180c5f3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/0b032fa77359745db793df5aff626779180c5f3b", + "reference": "0b032fa77359745db793df5aff626779180c5f3b", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.2" + }, + "conflict": { + "symfony/dotenv": "<6.4" + }, + "require-dev": { + "composer/composer": "^2.6", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Enables decoupling PHP applications from global state", + "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], + "support": { + "source": "https://github.com/symfony/runtime/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/security-bundle", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "6f6f859b437fb95028addfa21b417d25daca86d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/6f6f859b437fb95028addfa21b417d25daca86d5", + "reference": "6f6f859b437fb95028addfa21b417d25daca86d5", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/password-hasher": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/console": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-client": "<6.4", + "symfony/ldap": "<6.4", + "symfony/serializer": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.15", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-15T07:14:02+00:00" + }, + { + "name": "symfony/security-core", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "efff84605474ec682c7d9c6278088811e6f3caaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/efff84605474ec682c7d9c6278088811e6f3caaa", + "reference": "efff84605474ec682c7d9c6278088811e6f3caaa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/ldap": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-15T06:48:59+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "16b3aa2f67d02fb0dbd013a8759bbe90daaa9c5d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/16b3aa2f67d02fb0dbd013a8759bbe90daaa9c5d", + "reference": "16b3aa2f67d02fb0dbd013a8759bbe90daaa9c5d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/security-core": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/security-http", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "1fc7ca636cbd2cad29b42cc13c9fd0c681c6efee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/1fc7ca636cbd2cad29b42cc13c9fd0c681c6efee", + "reference": "1fc7ca636cbd2cad29b42cc13c9fd0c681c6efee", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.3|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/clock": "<6.4", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<6.4", + "symfony/security-csrf": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:04:42+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-06T11:19:24+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "81663873d946531129c76c65e80b681ce99c0e89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/81663873d946531129c76c65e80b681ce99c0e89", + "reference": "81663873d946531129c76c65e80b681ce99c0e89", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^3.21" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/console": "<6.4", + "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.4.37|>7,<7.4.9|>8.0,<8.0.9", + "symfony/serializer": "<6.4", + "symfony/translation": "<6.4", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4.37|^7.4.9|^8.0.9", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/workflow": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/cssinliner-extra": "^3", + "twig/inky-extra": "^3", + "twig/markdown-extra": "^3" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-29T17:13:54+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95", + "reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/twig-bridge": "^7.3|^8.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/framework-bundle": "<6.4", + "symfony/translation": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/type-info", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/cafeedbf157b890e94ac5b83eaed85595106d5d6", + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-22T15:21:55+00:00" + }, + { + "name": "symfony/validator", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/c76458623af9a3fe3b2e5b09b36453f334c2a361", + "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/var-exporter": "<6.4.25|>=7.0,<7.3.3", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/type-info": "^7.1.8", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-05T15:30:56+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:44:50+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/22e03a49c95ef054a43601cd159b222bfab1c701", + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/webpack-encore-bundle", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/webpack-encore-bundle.git", + "reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e", + "reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/asset": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/config": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/http-kernel": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0" + }, + "require-dev": { + "symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/http-client": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0 || ^8.0", + "symfony/web-link": "^5.4 || ^6.2 || ^7.0 || ^8.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/webpack-encore", + "name": "symfony/webpack-encore" + } + }, + "autoload": { + "psr-4": { + "Symfony\\WebpackEncoreBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration of your Symfony app with Webpack Encore", + "support": { + "issues": "https://github.com/symfony/webpack-encore-bundle/issues", + "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-27T13:41:46+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8b6952b56ca6417f25f7a65758cadd0ce02edc51", + "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "twig/twig", + "version": "v3.26.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "1fcae487b180d78e6351f4e0afa91f9eab96a2bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/1fcae487b180d78e6351f4e0afa91f9eab96a2bc", + "reference": "1fcae487b180d78e6351f4e0afa91f9eab96a2bc", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.26.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:31:59+00:00" + } + ], + "packages-dev": [ + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.67.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/6ce8b313845f16bcf385ee3cb31d8b24e30d5516", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1", + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.10|^3.0", + "doctrine/orm": "^2.15|^3", + "doctrine/persistence": "^3.1|^4.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.67.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-18T13:39:06+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/d9593c9efa40499eb078b81144de42cbc28a31f0", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T16:55:21+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.2", + "ext-ctype": "*", + "ext-iconv": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/httpdocs/config/bundles.php b/httpdocs/config/bundles.php new file mode 100644 index 0000000..2db66d9 --- /dev/null +++ b/httpdocs/config/bundles.php @@ -0,0 +1,12 @@ + ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], +]; diff --git a/httpdocs/config/packages/cache.yaml b/httpdocs/config/packages/cache.yaml new file mode 100644 index 0000000..6899b72 --- /dev/null +++ b/httpdocs/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/httpdocs/config/packages/csrf.yaml b/httpdocs/config/packages/csrf.yaml new file mode 100644 index 0000000..40d4040 --- /dev/null +++ b/httpdocs/config/packages/csrf.yaml @@ -0,0 +1,11 @@ +# Enable stateless CSRF protection for forms and logins/logouts +framework: + form: + csrf_protection: + token_id: submit + + csrf_protection: + stateless_token_ids: + - submit + - authenticate + - logout diff --git a/httpdocs/config/packages/doctrine.yaml b/httpdocs/config/packages/doctrine.yaml new file mode 100644 index 0000000..3b37d14 --- /dev/null +++ b/httpdocs/config/packages/doctrine.yaml @@ -0,0 +1,76 @@ +doctrine: + dbal: + default_connection: central + connections: + central: + url: '%env(resolve:DATABASE_URL)%' + profiling_collect_backtrace: '%kernel.debug%' + tenant: + # Basis-URL zeigt auf central – TenantConnectionMiddleware + # ersetzt dbname zur Laufzeit mit 'db_{slug}' + url: '%env(resolve:DATABASE_URL)%' + profiling_collect_backtrace: '%kernel.debug%' + # middlewares: + # - App\Doctrine\TenantConnectionMiddleware + + orm: + default_entity_manager: central + entity_managers: + central: + connection: central + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + mappings: + Central: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/Entity/Central' + prefix: 'App\Entity\Central' + alias: Central + tenant: + connection: tenant + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + mappings: + Tenant: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/Entity/Tenant' + prefix: 'App\Entity\Tenant' + alias: Tenant + +when@test: + doctrine: + dbal: + connections: + central: + url: '%env(resolve:DATABASE_URL)%' + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + tenant: + url: '%env(resolve:DATABASE_URL)%' + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + entity_managers: + central: + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + tenant: + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system \ No newline at end of file diff --git a/httpdocs/config/packages/doctrine_migrations.yaml b/httpdocs/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..fa79277 --- /dev/null +++ b/httpdocs/config/packages/doctrine_migrations.yaml @@ -0,0 +1,5 @@ +doctrine_migrations: + migrations_paths: + 'DoctrineMigrations': '%kernel.project_dir%/migrations/central' +# 'DoctrineMigrationsTenant': '%kernel.project_dir%/migrations/tenant' + enable_profiler: false \ No newline at end of file diff --git a/httpdocs/config/packages/framework.yaml b/httpdocs/config/packages/framework.yaml new file mode 100644 index 0000000..e6043c9 --- /dev/null +++ b/httpdocs/config/packages/framework.yaml @@ -0,0 +1,19 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + + # Note that the session will be started ONLY if you read or write from it. + session: + gc_maxlifetime: 14400 # 4 Stunden – serverseitige Sliding-Session + cookie_lifetime: 14400 # Cookie-Expiry beim ersten Setzen; Subscriber verlängert bei jedem Request + cookie_secure: auto + cookie_samesite: lax + + #esi: true + #fragments: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/httpdocs/config/packages/mailer.yaml b/httpdocs/config/packages/mailer.yaml new file mode 100644 index 0000000..18c68aa --- /dev/null +++ b/httpdocs/config/packages/mailer.yaml @@ -0,0 +1,5 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' + headers: + from: 'spawntree Timetracker ' \ No newline at end of file diff --git a/httpdocs/config/packages/monolog.yaml b/httpdocs/config/packages/monolog.yaml new file mode 100644 index 0000000..383a9ea --- /dev/null +++ b/httpdocs/config/packages/monolog.yaml @@ -0,0 +1,20 @@ +# config/packages/monolog.yaml + +when@dev: + monolog: + handlers: + main: + type: stream + path: '%kernel.logs_dir%/%kernel.environment%.log' + level: warning + channels: ['!event'] + +when@prod: + monolog: + handlers: + main: + type: rotating_file + path: '%kernel.logs_dir%/%kernel.environment%.log' + level: warning + max_files: 10 + channels: ['!event'] \ No newline at end of file diff --git a/httpdocs/config/packages/property_info.yaml b/httpdocs/config/packages/property_info.yaml new file mode 100644 index 0000000..dd31b9d --- /dev/null +++ b/httpdocs/config/packages/property_info.yaml @@ -0,0 +1,3 @@ +framework: + property_info: + with_constructor_extractor: true diff --git a/httpdocs/config/packages/routing.yaml b/httpdocs/config/packages/routing.yaml new file mode 100644 index 0000000..0f34f87 --- /dev/null +++ b/httpdocs/config/packages/routing.yaml @@ -0,0 +1,10 @@ +framework: + router: + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + default_uri: '%env(DEFAULT_URI)%' + +when@prod: + framework: + router: + strict_requirements: null diff --git a/httpdocs/config/packages/security.yaml b/httpdocs/config/packages/security.yaml new file mode 100644 index 0000000..e37466b --- /dev/null +++ b/httpdocs/config/packages/security.yaml @@ -0,0 +1,65 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + App\Entity\Central\User: + algorithm: auto + + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + app_user_provider: + entity: + class: App\Entity\Central\User + property: email + # users_in_memory: { memory: null } + + firewalls: + dev: + # Ensure dev tools and static assets are always allowed + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: app_user_provider + access_denied_handler: App\Security\AccessDeniedHandler + form_login: + login_path: app_login + check_path: app_login + default_target_path: /week + username_parameter: email + password_parameter: password + enable_csrf: true + logout: + path: app_logout + target: app_login + remember_me: + secret: '%kernel.secret%' + lifetime: 2592000 # 30 Tage + path: / + name: REMEMBERME + + # Activate different ways to authenticate: + # https://symfony.com/doc/current/security.html#the-firewall + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + # Note: Only the *first* matching rule is applied + access_control: + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/register, roles: PUBLIC_ACCESS } + - { path: ^/api/register, roles: PUBLIC_ACCESS } + - { path: ^/verify/, roles: PUBLIC_ACCESS } + - { path: ^/invite/, roles: PUBLIC_ACCESS } + - { path: ^/$, roles: PUBLIC_ACCESS } + - { path: ^/, roles: ROLE_USER } + +when@test: + security: + password_hashers: + # Password hashers are resource-intensive by design to ensure security. + # In tests, it's safe to reduce their cost to improve performance. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/httpdocs/config/packages/translation.yaml b/httpdocs/config/packages/translation.yaml new file mode 100644 index 0000000..da2c305 --- /dev/null +++ b/httpdocs/config/packages/translation.yaml @@ -0,0 +1,7 @@ +framework: + default_locale: de + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - de + providers: \ No newline at end of file diff --git a/httpdocs/config/packages/twig.yaml b/httpdocs/config/packages/twig.yaml new file mode 100644 index 0000000..3f795d9 --- /dev/null +++ b/httpdocs/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + file_name_pattern: '*.twig' + +when@test: + twig: + strict_variables: true diff --git a/httpdocs/config/packages/validator.yaml b/httpdocs/config/packages/validator.yaml new file mode 100644 index 0000000..dd47a6a --- /dev/null +++ b/httpdocs/config/packages/validator.yaml @@ -0,0 +1,11 @@ +framework: + validation: + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/httpdocs/config/packages/webpack_encore.yaml b/httpdocs/config/packages/webpack_encore.yaml new file mode 100644 index 0000000..4c009ee --- /dev/null +++ b/httpdocs/config/packages/webpack_encore.yaml @@ -0,0 +1,45 @@ +webpack_encore: + # The path where Encore is building the assets - i.e. Encore.setOutputPath() + output_path: '%kernel.project_dir%/public/build' + # If multiple builds are defined (as shown below), you can disable the default build: + # output_path: false + + # Set attributes that will be rendered on all script and link tags + script_attributes: + defer: true + # Uncomment (also under link_attributes) if using Turbo Drive + # https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change + # 'data-turbo-track': reload + # link_attributes: + # Uncomment if using Turbo Drive + # 'data-turbo-track': reload + + # If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') + # crossorigin: 'anonymous' + + # Preload all rendered script and link tags automatically via the HTTP/2 Link header + # preload: true + + # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data + # strict_mode: false + + # If you have multiple builds: + # builds: + # frontend: '%kernel.project_dir%/public/frontend/build' + + # pass the build name as the 3rd argument to the Twig functions + # {{ encore_entry_script_tags('entry1', null, 'frontend') }} + +framework: + assets: + json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' + +#when@prod: +# webpack_encore: +# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) +# # Available in version 1.2 +# cache: true + +#when@test: +# webpack_encore: +# strict_mode: false diff --git a/httpdocs/config/preload.php b/httpdocs/config/preload.php new file mode 100644 index 0000000..5ebcdb2 --- /dev/null +++ b/httpdocs/config/preload.php @@ -0,0 +1,5 @@ + [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|Param|null>|Param|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: CallbackType, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: InstanceofType, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type FrameworkConfig = array{ + * secret?: scalar|Param|null, + * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false + * allowed_http_method_override?: null|list, + * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" + * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" + * test?: bool|Param, + * default_locale?: scalar|Param|null, // Default: "en" + * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false + * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false + * enabled_locales?: list, + * trusted_hosts?: string|list, + * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] + * trusted_headers?: string|list, + * error_controller?: scalar|Param|null, // Default: "error_controller" + * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * stateless_token_ids?: list, + * check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false + * cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" + * }, + * form?: bool|array{ // Form configuration + * enabled?: bool|Param, // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * token_id?: scalar|Param|null, // Default: null + * field_name?: scalar|Param|null, // Default: "_token" + * field_attr?: array, + * }, + * }, + * http_cache?: bool|array{ // HTTP cache configuration + * enabled?: bool|Param, // Default: false + * debug?: bool|Param, // Default: "%kernel.debug%" + * trace_level?: "none"|"short"|"full"|Param, + * trace_header?: scalar|Param|null, + * default_ttl?: int|Param, + * private_headers?: list, + * skip_response_headers?: list, + * allow_reload?: bool|Param, + * allow_revalidate?: bool|Param, + * stale_while_revalidate?: int|Param, + * stale_if_error?: int|Param, + * terminate_on_cache_hit?: bool|Param, + * }, + * esi?: bool|array{ // ESI configuration + * enabled?: bool|Param, // Default: false + * }, + * ssi?: bool|array{ // SSI configuration + * enabled?: bool|Param, // Default: false + * }, + * fragments?: bool|array{ // Fragments configuration + * enabled?: bool|Param, // Default: false + * hinclude_default_template?: scalar|Param|null, // Default: null + * path?: scalar|Param|null, // Default: "/_fragment" + * }, + * profiler?: bool|array{ // Profiler configuration + * enabled?: bool|Param, // Default: false + * collect?: bool|Param, // Default: true + * collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null + * only_exceptions?: bool|Param, // Default: false + * only_main_requests?: bool|Param, // Default: false + * dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler" + * collect_serializer_data?: bool|Param, // Enables the serializer data collector and profiler panel. // Default: false + * }, + * workflows?: bool|array{ + * enabled?: bool|Param, // Default: false + * workflows?: array, + * definition_validators?: list, + * support_strategy?: scalar|Param|null, + * initial_marking?: \BackedEnum|string|list, + * events_to_dispatch?: null|list, + * places?: string|list, + * }>, + * transitions?: list, + * to?: \BackedEnum|string|list, + * weight?: int|Param, // Default: 1 + * metadata?: array, + * }>, + * metadata?: array, + * }>, + * }, + * router?: bool|array{ // Router configuration + * enabled?: bool|Param, // Default: false + * resource?: scalar|Param|null, + * type?: scalar|Param|null, + * cache_dir?: scalar|Param|null, // Deprecated: Setting the "framework.router.cache_dir.cache_dir" configuration option is deprecated. It will be removed in version 8.0. // Default: "%kernel.build_dir%" + * default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null + * http_port?: scalar|Param|null, // Default: 80 + * https_port?: scalar|Param|null, // Default: 443 + * strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true + * utf8?: bool|Param, // Default: true + * }, + * session?: bool|array{ // Session configuration + * enabled?: bool|Param, // Default: false + * storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native" + * handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. + * name?: scalar|Param|null, + * cookie_lifetime?: scalar|Param|null, + * cookie_path?: scalar|Param|null, + * cookie_domain?: scalar|Param|null, + * cookie_secure?: true|false|"auto"|Param, // Default: "auto" + * cookie_httponly?: bool|Param, // Default: true + * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * use_cookies?: bool|Param, + * gc_divisor?: scalar|Param|null, + * gc_probability?: scalar|Param|null, + * gc_maxlifetime?: scalar|Param|null, + * save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. + * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0 + * sid_length?: int|Param, // Deprecated: Setting the "framework.session.sid_length.sid_length" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. + * sid_bits_per_character?: int|Param, // Deprecated: Setting the "framework.session.sid_bits_per_character.sid_bits_per_character" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. + * }, + * request?: bool|array{ // Request configuration + * enabled?: bool|Param, // Default: false + * formats?: array>, + * }, + * assets?: bool|array{ // Assets configuration + * enabled?: bool|Param, // Default: true + * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|Param|null, // Default: null + * version?: scalar|Param|null, // Default: null + * version_format?: scalar|Param|null, // Default: "%%s?%%s" + * json_manifest_path?: scalar|Param|null, // Default: null + * base_path?: scalar|Param|null, // Default: "" + * base_urls?: string|list, + * packages?: array, + * }>, + * }, + * asset_mapper?: bool|array{ // Asset Mapper configuration + * enabled?: bool|Param, // Default: false + * paths?: string|array, + * excluded_patterns?: list, + * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true + * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true + * public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" + * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" + * extensions?: array, + * importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" + * importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" + * importmap_script_attributes?: array, + * vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" + * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. + * enabled?: bool|Param, // Default: false + * formats?: list, + * extensions?: list, + * }, + * }, + * translator?: bool|array{ // Translator configuration + * enabled?: bool|Param, // Default: true + * fallbacks?: string|list, + * logging?: bool|Param, // Default: false + * formatter?: scalar|Param|null, // Default: "translator.formatter.default" + * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" + * default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" + * paths?: list, + * pseudo_localization?: bool|array{ + * enabled?: bool|Param, // Default: false + * accents?: bool|Param, // Default: true + * expansion_factor?: float|Param, // Default: 1.0 + * brackets?: bool|Param, // Default: true + * parse_html?: bool|Param, // Default: false + * localizable_html_attributes?: list, + * }, + * providers?: array, + * locales?: list, + * }>, + * globals?: array, + * domain?: string|Param, + * }>, + * }, + * validation?: bool|array{ // Validation configuration + * enabled?: bool|Param, // Default: true + * cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0. + * enable_attributes?: bool|Param, // Default: true + * static_method?: string|list, + * translation_domain?: scalar|Param|null, // Default: "validators" + * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5" + * mapping?: array{ + * paths?: list, + * }, + * not_compromised_password?: bool|array{ + * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true + * endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null + * }, + * disable_translation?: bool|Param, // Default: false + * auto_mapping?: array, + * }>, + * }, + * annotations?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * serializer?: bool|array{ // Serializer configuration + * enabled?: bool|Param, // Default: false + * enable_attributes?: bool|Param, // Default: true + * name_converter?: scalar|Param|null, + * circular_reference_handler?: scalar|Param|null, + * max_depth_handler?: scalar|Param|null, + * mapping?: array{ + * paths?: list, + * }, + * default_context?: array, + * named_serializers?: array, + * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true + * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true + * }>, + * }, + * property_access?: bool|array{ // Property access configuration + * enabled?: bool|Param, // Default: true + * magic_call?: bool|Param, // Default: false + * magic_get?: bool|Param, // Default: true + * magic_set?: bool|Param, // Default: true + * throw_exception_on_invalid_index?: bool|Param, // Default: false + * throw_exception_on_invalid_property_path?: bool|Param, // Default: true + * }, + * type_info?: bool|array{ // Type info configuration + * enabled?: bool|Param, // Default: true + * aliases?: array, + * }, + * property_info?: bool|array{ // Property info configuration + * enabled?: bool|Param, // Default: true + * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. + * }, + * cache?: array{ // Cache configuration + * prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" + * app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem" + * system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system" + * directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app" + * default_psr6_provider?: scalar|Param|null, + * default_redis_provider?: scalar|Param|null, // Default: "redis://localhost" + * default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost" + * default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost" + * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" + * default_pdo_provider?: scalar|Param|null, // Default: null + * pools?: array, + * tags?: scalar|Param|null, // Default: null + * public?: bool|Param, // Default: false + * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. + * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|Param|null, + * clearer?: scalar|Param|null, + * }>, + * }, + * php_errors?: array{ // PHP errors handling configuration + * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true + * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true + * }, + * exceptions?: array, + * web_link?: bool|array{ // Web links configuration + * enabled?: bool|Param, // Default: false + * }, + * lock?: bool|string|array{ // Lock configuration + * enabled?: bool|Param, // Default: false + * resources?: string|array>, + * }, + * semaphore?: bool|string|array{ // Semaphore configuration + * enabled?: bool|Param, // Default: false + * resources?: string|array, + * }, + * messenger?: bool|array{ // Messenger configuration + * enabled?: bool|Param, // Default: false + * routing?: array, + * }>, + * serializer?: array{ + * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" + * symfony_serializer?: array{ + * format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" + * context?: array, + * }, + * }, + * transports?: array, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: string|array{ + * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 + * }, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null + * }>, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * stop_worker_on_signals?: int|string|list, + * default_bus?: scalar|Param|null, // Default: null + * buses?: array, + * }>, + * }>, + * }, + * scheduler?: bool|array{ // Scheduler configuration + * enabled?: bool|Param, // Default: false + * }, + * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true + * http_client?: bool|array{ // HTTP Client configuration + * enabled?: bool|Param, // Default: false + * max_host_connections?: int|Param, // The maximum number of connections to a single host. + * default_options?: array{ + * headers?: array, + * vars?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...) + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }, + * mock_response_factory?: scalar|Param|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable. + * scoped_clients?: array, + * headers?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }>, + * }, + * mailer?: bool|array{ // Mailer configuration + * enabled?: bool|Param, // Default: true + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * dsn?: scalar|Param|null, // Default: null + * transports?: array, + * envelope?: array{ // Mailer Envelope configuration + * sender?: scalar|Param|null, + * recipients?: string|list, + * allowed_recipients?: string|list, + * }, + * headers?: array, + * dkim_signer?: bool|array{ // DKIM signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" + * domain?: scalar|Param|null, // Default: "" + * select?: scalar|Param|null, // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: "" + * options?: array, + * }, + * smime_signer?: bool|array{ // S/MIME signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Path to key (in PEM format) // Default: "" + * certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: null + * extra_certificates?: scalar|Param|null, // Default: null + * sign_options?: int|Param, // Default: null + * }, + * smime_encrypter?: bool|array{ // S/MIME encrypter configuration + * enabled?: bool|Param, // Default: false + * repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" + * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null + * }, + * }, + * secrets?: bool|array{ + * enabled?: bool|Param, // Default: true + * vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" + * local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local" + * decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" + * }, + * notifier?: bool|array{ // Notifier configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * chatter_transports?: array, + * texter_transports?: array, + * notification_on_failed_messages?: bool|Param, // Default: false + * channel_policy?: array>, + * admin_recipients?: list, + * }, + * rate_limiter?: bool|array{ // Rate limiter configuration + * enabled?: bool|Param, // Default: false + * limiters?: array, + * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". + * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 + * }, + * }>, + * }, + * uid?: bool|array{ // Uid configuration + * enabled?: bool|Param, // Default: false + * default_uuid_version?: 7|6|4|1|Param, // Default: 7 + * name_based_uuid_version?: 5|3|Param, // Default: 5 + * name_based_uuid_namespace?: scalar|Param|null, + * time_based_uuid_version?: 7|6|1|Param, // Default: 7 + * time_based_uuid_node?: scalar|Param|null, + * }, + * html_sanitizer?: bool|array{ // HtmlSanitizer configuration + * enabled?: bool|Param, // Default: false + * sanitizers?: array, + * block_elements?: string|list, + * drop_elements?: string|list, + * allow_attributes?: array, + * drop_attributes?: array, + * force_attributes?: array>, + * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false + * allowed_link_schemes?: string|list, + * allowed_link_hosts?: null|string|list, + * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false + * allowed_media_schemes?: string|list, + * allowed_media_hosts?: null|string|list, + * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false + * with_attribute_sanitizers?: string|list, + * without_attribute_sanitizers?: string|list, + * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 + * }>, + * }, + * webhook?: bool|array{ // Webhook configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" + * routing?: array, + * }, + * remote-event?: bool|array{ // RemoteEvent configuration + * enabled?: bool|Param, // Default: false + * }, + * json_streamer?: bool|array{ // JSON streamer configuration + * enabled?: bool|Param, // Default: false + * }, + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array, + * autoescape_service?: scalar|Param|null, // Default: null + * autoescape_service_method?: scalar|Param|null, // Default: null + * base_template_class?: scalar|Param|null, // Deprecated: The child node "base_template_class" at path "twig.base_template_class" is deprecated. + * cache?: scalar|Param|null, // Default: true + * charset?: scalar|Param|null, // Default: "%kernel.charset%" + * debug?: bool|Param, // Default: "%kernel.debug%" + * strict_variables?: bool|Param, // Default: "%kernel.debug%" + * auto_reload?: scalar|Param|null, + * optimizations?: int|Param, + * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: string|list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|Param|null, // Default: "F j, Y H:i" + * interval_format?: scalar|Param|null, // Default: "%d days" + * timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int|Param, // Default: 0 + * decimal_point?: scalar|Param|null, // Default: "." + * thousands_separator?: scalar|Param|null, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } + * @psalm-type DoctrineConfig = array{ + * dbal?: array{ + * default_connection?: scalar|Param|null, + * types?: array, + * driver_schemes?: array, + * connections?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|Param|null, + * replicas?: array, + * }>, + * }, + * orm?: array{ + * default_entity_manager?: scalar|Param|null, + * enable_native_lazy_objects?: bool|Param, // Deprecated: The "enable_native_lazy_objects" option is deprecated and will be removed in DoctrineBundle 4.0, as native lazy objects are now always enabled. // Default: true + * controller_resolver?: bool|array{ + * enabled?: bool|Param, // Default: true + * auto_mapping?: bool|Param, // Deprecated: The "doctrine.orm.controller_resolver.auto_mapping.auto_mapping" option is deprecated and will be removed in DoctrineBundle 4.0, as it only accepts `false` since 3.0. // Set to true to enable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: false + * evict_cache?: bool|Param, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false + * }, + * entity_managers?: array, + * }>, + * }>, + * }, + * connection?: scalar|Param|null, + * class_metadata_factory_name?: scalar|Param|null, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|Param|null, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|Param|null, // Default: false + * naming_strategy?: scalar|Param|null, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|Param|null, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|Param|null, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|Param|null, // Default: null + * fetch_mode_subselect_batch_size?: scalar|Param|null, + * repository_factory?: scalar|Param|null, // Default: "doctrine.orm.container_repository_factory" + * schema_ignore_classes?: list, + * validate_xml_mapping?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/6728. // Default: false + * second_level_cache?: array{ + * region_cache_driver?: string|array{ + * type?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * pool?: scalar|Param|null, + * }, + * region_lock_lifetime?: scalar|Param|null, // Default: 60 + * log_enabled?: bool|Param, // Default: true + * region_lifetime?: scalar|Param|null, // Default: 3600 + * enabled?: bool|Param, // Default: true + * factory?: scalar|Param|null, + * regions?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * }>, + * identity_generation_preferences?: array, + * }>, + * resolve_target_entities?: array, + * }, + * } + * @psalm-type DoctrineMigrationsConfig = array{ + * enable_service_migrations?: bool|Param, // Whether to enable fetching migrations from the service container. // Default: false + * migrations_paths?: array, + * services?: array, + * factories?: array, + * storage?: array{ // Storage to use for migration status metadata. + * table_storage?: array{ // The default metadata storage, implemented as a table in the database. + * table_name?: scalar|Param|null, // Default: null + * version_column_name?: scalar|Param|null, // Default: null + * version_column_length?: scalar|Param|null, // Default: null + * executed_at_column_name?: scalar|Param|null, // Default: null + * execution_time_column_name?: scalar|Param|null, // Default: null + * }, + * }, + * migrations?: list, + * connection?: scalar|Param|null, // Connection name to use for the migrations database. // Default: null + * em?: scalar|Param|null, // Entity manager name to use for the migrations database (available when doctrine/orm is installed). // Default: null + * all_or_nothing?: scalar|Param|null, // Run all migrations in a transaction. // Default: false + * check_database_platform?: scalar|Param|null, // Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on. // Default: true + * custom_template?: scalar|Param|null, // Custom template path for generated migration classes. // Default: null + * organize_migrations?: scalar|Param|null, // Organize migrations mode. Possible values are: "BY_YEAR", "BY_YEAR_AND_MONTH", false // Default: false + * enable_profiler?: bool|Param, // Whether or not to enable the profiler collector to calculate and visualize migration status. This adds some queries overhead. // Default: false + * transactional?: bool|Param, // Whether or not to wrap migrations in a single transaction. // Default: true + * } + * @psalm-type MakerConfig = array{ + * root_namespace?: scalar|Param|null, // Default: "App" + * generate_final_classes?: bool|Param, // Default: true + * generate_final_entities?: bool|Param, // Default: false + * } + * @psalm-type SecurityConfig = array{ + * access_denied_url?: scalar|Param|null, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate"|Param, // Default: "migrate" + * hide_user_not_found?: bool|Param, // Deprecated: The "hide_user_not_found" option is deprecated and will be removed in 8.0. Use the "expose_security_errors" option instead. + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All|Param, // Default: "none" + * erase_credentials?: bool|Param, // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority"|Param, + * service?: scalar|Param|null, + * strategy_service?: scalar|Param|null, + * allow_if_all_abstain?: bool|Param, // Default: false + * allow_if_equal_granted_denied?: bool|Param, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" + * key_length?: scalar|Param|null, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|Param|null, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|Param|null, // Default: null + * time_cost?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * }>, + * providers?: array, + * }, + * entity?: array{ + * class?: scalar|Param|null, // The full entity class name of your user class. + * property?: scalar|Param|null, // Default: null + * manager_name?: scalar|Param|null, // Default: null + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service?: scalar|Param|null, + * base_dn?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: null + * search_password?: scalar|Param|null, // Default: null + * extra_fields?: list, + * default_roles?: string|list, + * role_fetcher?: scalar|Param|null, // Default: null + * uid_key?: scalar|Param|null, // Default: "sAMAccountName" + * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|Param|null, // Default: null + * }, + * }>, + * firewalls?: array, + * security?: bool|Param, // Default: true + * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|Param|null, + * access_denied_url?: scalar|Param|null, + * access_denied_handler?: scalar|Param|null, + * entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|Param|null, + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|Param|null, + * logout?: array{ + * enable_csrf?: bool|Param|null, // Default: null + * csrf_token_id?: scalar|Param|null, // Default: "logout" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_manager?: scalar|Param|null, + * path?: scalar|Param|null, // Default: "/logout" + * target?: scalar|Param|null, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: string|list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, + * delete_cookies?: string|array, + * }, + * switch_user?: array{ + * provider?: scalar|Param|null, + * parameter?: scalar|Param|null, // Default: "_switch_user" + * role?: scalar|Param|null, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|Param|null, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|Param|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|Param|null, // Default: "1 minute" + * lock_factory?: scalar|Param|null, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null + * cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * x509?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|Param|null, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "REMOTE_USER" + * }, + * login_link?: array{ + * check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|Param|null, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties?: list, + * lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|Param|null, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|Param|null, // The user provider to load users from. + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * login_path?: scalar|Param|null, // Default: "/login" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: null + * token_extractors?: string|list, + * token_handler?: string|array{ + * id?: scalar|Param|null, + * oidc_user_info?: string|array{ + * base_uri?: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured). + * discovery?: array{ // Enable the OIDC discovery. + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|Param|null, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri?: string|list, + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience?: scalar|Param|null, // Audience set in the token, for validation purpose. + * issuers?: list, + * algorithm?: array, + * algorithms?: list, + * key?: scalar|Param|null, // Deprecated: The "key" option is deprecated and will be removed in 8.0. Use the "keyset" option instead. // JSON-encoded JWK used to sign the token (must contain a "kty" key). + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool|Param, // Default: false + * enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false + * algorithms?: list, + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url?: scalar|Param|null, // CAS server validation URL + * prefix?: scalar|Param|null, // CAS prefix // Default: "cas" + * http_client?: scalar|Param|null, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|Param|null, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * service?: scalar|Param|null, + * user_providers?: string|list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|Param|null, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|Param|null, // Default: null + * }, + * }, + * token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier. + * name?: scalar|Param|null, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|Param|null, // Default: "/" + * domain?: scalar|Param|null, // Default: null + * secure?: true|false|"auto"|Param, // Default: null + * httponly?: bool|Param, // Default: true + * samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * always_remember_me?: bool|Param, // Default: false + * remember_me_parameter?: scalar|Param|null, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|Param|null, // Default: null + * methods?: string|list, + * allow_if?: scalar|Param|null, // Default: null + * roles?: string|list, + * }>, + * role_hierarchy?: array>, + * } + * @psalm-type WebpackEncoreConfig = array{ + * output_path?: scalar|Param|null, // The path where Encore is building the assets - i.e. Encore.setOutputPath() + * crossorigin?: false|"anonymous"|"use-credentials"|Param, // crossorigin value when Encore.enableIntegrityHashes() is used, can be false (default), anonymous or use-credentials // Default: false + * preload?: bool|Param, // preload all rendered script and link tags automatically via the http2 Link header. // Default: false + * cache?: bool|Param, // Enable caching of the entry point file(s) // Default: false + * strict_mode?: bool|Param, // Throw an exception if the entrypoints.json file is missing or an entry is missing from the data // Default: true + * builds?: array, + * script_attributes?: array, + * link_attributes?: array, + * } + * @psalm-type MonologConfig = array{ + * use_microseconds?: scalar|Param|null, // Default: true + * channels?: list, + * handlers?: array, + * }>, + * accepted_levels?: list, + * min_level?: scalar|Param|null, // Default: "DEBUG" + * max_level?: scalar|Param|null, // Default: "EMERGENCY" + * buffer_size?: scalar|Param|null, // Default: 0 + * flush_on_overflow?: bool|Param, // Default: false + * handler?: scalar|Param|null, + * url?: scalar|Param|null, + * exchange?: scalar|Param|null, + * exchange_name?: scalar|Param|null, // Default: "log" + * channel?: scalar|Param|null, // Default: null + * bot_name?: scalar|Param|null, // Default: "Monolog" + * use_attachment?: scalar|Param|null, // Default: true + * use_short_attachment?: scalar|Param|null, // Default: false + * include_extra?: scalar|Param|null, // Default: false + * icon_emoji?: scalar|Param|null, // Default: null + * webhook_url?: scalar|Param|null, + * exclude_fields?: list, + * token?: scalar|Param|null, + * region?: scalar|Param|null, + * source?: scalar|Param|null, + * use_ssl?: bool|Param, // Default: true + * user?: mixed, + * title?: scalar|Param|null, // Default: null + * host?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 514 + * config?: list, + * members?: list, + * connection_string?: scalar|Param|null, + * timeout?: scalar|Param|null, + * time?: scalar|Param|null, // Default: 60 + * deduplication_level?: scalar|Param|null, // Default: 400 + * store?: scalar|Param|null, // Default: null + * connection_timeout?: scalar|Param|null, + * persistent?: bool|Param, + * message_type?: scalar|Param|null, // Default: 0 + * parse_mode?: scalar|Param|null, // Default: null + * disable_webpage_preview?: bool|Param|null, // Default: null + * disable_notification?: bool|Param|null, // Default: null + * split_long_messages?: bool|Param, // Default: false + * delay_between_messages?: bool|Param, // Default: false + * topic?: int|Param, // Default: null + * factor?: int|Param, // Default: 1 + * tags?: string|list, + * console_formatter_options?: mixed, // Default: [] + * formatter?: scalar|Param|null, + * nested?: bool|Param, // Default: false + * publisher?: string|array{ + * id?: scalar|Param|null, + * hostname?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 12201 + * chunk_size?: scalar|Param|null, // Default: 1420 + * encoder?: "json"|"compressed_json"|Param, + * }, + * mongodb?: string|array{ + * id?: scalar|Param|null, // ID of a MongoDB\Client service + * uri?: scalar|Param|null, + * username?: scalar|Param|null, + * password?: scalar|Param|null, + * database?: scalar|Param|null, // Default: "monolog" + * collection?: scalar|Param|null, // Default: "logs" + * }, + * elasticsearch?: string|array{ + * id?: scalar|Param|null, + * hosts?: list, + * host?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 9200 + * transport?: scalar|Param|null, // Default: "Http" + * user?: scalar|Param|null, // Default: null + * password?: scalar|Param|null, // Default: null + * }, + * index?: scalar|Param|null, // Default: "monolog" + * document_type?: scalar|Param|null, // Default: "logs" + * ignore_error?: scalar|Param|null, // Default: false + * redis?: string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * password?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 6379 + * database?: scalar|Param|null, // Default: 0 + * key_name?: scalar|Param|null, // Default: "monolog_redis" + * }, + * predis?: string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * }, + * from_email?: scalar|Param|null, + * to_email?: string|list, + * subject?: scalar|Param|null, + * content_type?: scalar|Param|null, // Default: null + * headers?: list, + * mailer?: scalar|Param|null, // Default: null + * email_prototype?: string|array{ + * id?: scalar|Param|null, + * method?: scalar|Param|null, // Default: null + * }, + * verbosity_levels?: array{ + * VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR" + * VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING" + * VERBOSITY_VERBOSE?: scalar|Param|null, // Default: "NOTICE" + * VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: "INFO" + * VERBOSITY_DEBUG?: scalar|Param|null, // Default: "DEBUG" + * }, + * channels?: string|array{ + * type?: scalar|Param|null, + * elements?: list, + * }, + * }>, + * } + * @psalm-type ConfigType = array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * webpack_encore?: WebpackEncoreConfig, + * monolog?: MonologConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * maker?: MakerConfig, + * security?: SecurityConfig, + * webpack_encore?: WebpackEncoreConfig, + * monolog?: MonologConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * webpack_encore?: WebpackEncoreConfig, + * monolog?: MonologConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * webpack_encore?: WebpackEncoreConfig, + * monolog?: MonologConfig, + * }, + * ..., + * }> + * } + */ +final class App +{ + /** + * @param ConfigType $config + * + * @psalm-return ConfigType + */ + public static function config(array $config): array + { + /** @var ConfigType $config */ + $config = AppReference::config($config); + + return $config; + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/httpdocs/config/routes.yaml b/httpdocs/config/routes.yaml new file mode 100644 index 0000000..cef258c --- /dev/null +++ b/httpdocs/config/routes.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json + +# This file is the entry point to configure the routes of your app. +# Methods with the #[Route] attribute are automatically imported. +# See also https://symfony.com/doc/current/routing.html + +# To list all registered routes, run the following command: +# bin/console debug:router + +controllers: + resource: routing.controllers diff --git a/httpdocs/config/routes/framework.yaml b/httpdocs/config/routes/framework.yaml new file mode 100644 index 0000000..bc1feac --- /dev/null +++ b/httpdocs/config/routes/framework.yaml @@ -0,0 +1,4 @@ +when@dev: + _errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.php' + prefix: /_error diff --git a/httpdocs/config/routes/security.yaml b/httpdocs/config/routes/security.yaml new file mode 100644 index 0000000..f853be1 --- /dev/null +++ b/httpdocs/config/routes/security.yaml @@ -0,0 +1,3 @@ +_security_logout: + resource: security.route_loader.logout + type: service diff --git a/httpdocs/config/services.yaml b/httpdocs/config/services.yaml new file mode 100644 index 0000000..a62cb52 --- /dev/null +++ b/httpdocs/config/services.yaml @@ -0,0 +1,60 @@ +# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json + +parameters: + app.domain: '%env(APP_DOMAIN)%' + +services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../src/' + + # ── Tenant EM explizit binden ────────────────────────────────────────────── + # Symfony kann bei mehreren EMs nicht automatisch wissen welcher gemeint ist. + # Alle Services/Controller die Tenant-Entities anfassen, brauchen den tenant EM. + + App\Controller\TimeTrackingController: + arguments: + $tenantEm: '@doctrine.orm.tenant_entity_manager' + + App\Controller\ClientController: + arguments: + $em: '@doctrine.orm.tenant_entity_manager' + + App\Controller\ProjectController: + arguments: + $em: '@doctrine.orm.tenant_entity_manager' + + App\Controller\ServiceController: + arguments: + $em: '@doctrine.orm.tenant_entity_manager' + + App\Command\SeedCommand: + arguments: + $centralEm: '@doctrine.orm.central_entity_manager' + $tenantEm: '@doctrine.orm.tenant_entity_manager' + + # ── app.domain in Subscriber injizieren ─────────────────────────────────── + App\EventSubscriber\TenantRequestSubscriber: + arguments: + $appDomain: '%app.domain%' + + App\Controller\RegistrationController: + arguments: + $appDomain: '%app.domain%' + + App\Service\RegistrationService: + arguments: + $centralEm: '@doctrine.orm.central_entity_manager' + $tenantEm: '@doctrine.orm.tenant_entity_manager' + $appDomain: '%app.domain%' + $notifyEmail: '%env(REGISTRATION_NOTIFY_EMAIL)%' + + App\Controller\TeamController: + arguments: + $appDomain: '%app.domain%' + + App\Controller\InviteController: + arguments: ~ diff --git a/httpdocs/migrations/.gitignore b/httpdocs/migrations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/httpdocs/migrations/central/.gitkeep b/httpdocs/migrations/central/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/httpdocs/migrations/central/Version20260523122322.php b/httpdocs/migrations/central/Version20260523122322.php new file mode 100644 index 0000000..1841148 --- /dev/null +++ b/httpdocs/migrations/central/Version20260523122322.php @@ -0,0 +1,39 @@ +addSql('CREATE TABLE account (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(63) NOT NULL, created_at DATETIME NOT NULL, UNIQUE INDEX UNIQ_7D3656A4989D9B62 (slug), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE account_user (id INT AUTO_INCREMENT NOT NULL, role VARCHAR(20) NOT NULL, account_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_10051E39B6B5FBA (account_id), INDEX IDX_10051E3A76ED395 (user_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, password VARCHAR(255) DEFAULT NULL, note LONGTEXT DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE account_user ADD CONSTRAINT FK_10051E39B6B5FBA FOREIGN KEY (account_id) REFERENCES account (id)'); + $this->addSql('ALTER TABLE account_user ADD CONSTRAINT FK_10051E3A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE account_user DROP FOREIGN KEY FK_10051E39B6B5FBA'); + $this->addSql('ALTER TABLE account_user DROP FOREIGN KEY FK_10051E3A76ED395'); + $this->addSql('DROP TABLE account'); + $this->addSql('DROP TABLE account_user'); + $this->addSql('DROP TABLE `user`'); + } +} diff --git a/httpdocs/migrations/central/Version20260523190211.php b/httpdocs/migrations/central/Version20260523190211.php new file mode 100644 index 0000000..5e25a16 --- /dev/null +++ b/httpdocs/migrations/central/Version20260523190211.php @@ -0,0 +1,31 @@ +addSql('CREATE TABLE registration_token (id INT AUTO_INCREMENT NOT NULL, token VARCHAR(64) NOT NULL, company_name VARCHAR(255) NOT NULL, slug VARCHAR(63) NOT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, expires_at DATETIME NOT NULL, UNIQUE INDEX UNIQ_D09D01D35F37A13B (token), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE registration_token'); + } +} diff --git a/httpdocs/migrations/central/Version20260523203200.php b/httpdocs/migrations/central/Version20260523203200.php new file mode 100644 index 0000000..5b90065 --- /dev/null +++ b/httpdocs/migrations/central/Version20260523203200.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE account ADD tracking_interval SMALLINT DEFAULT 1 NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE account DROP tracking_interval'); + } +} diff --git a/httpdocs/migrations/central/Version20260523212725.php b/httpdocs/migrations/central/Version20260523212725.php new file mode 100644 index 0000000..0b2cfd3 --- /dev/null +++ b/httpdocs/migrations/central/Version20260523212725.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE invite_token (id INT AUTO_INCREMENT NOT NULL, token VARCHAR(64) NOT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, expires_at DATETIME NOT NULL, account_id INT NOT NULL, UNIQUE INDEX UNIQ_5242FFC45F37A13B (token), INDEX IDX_5242FFC49B6B5FBA (account_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE invite_token ADD CONSTRAINT FK_5242FFC49B6B5FBA FOREIGN KEY (account_id) REFERENCES account (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE invite_token DROP FOREIGN KEY FK_5242FFC49B6B5FBA'); + $this->addSql('DROP TABLE invite_token'); + } +} diff --git a/httpdocs/migrations/central/Version20260523221257.php b/httpdocs/migrations/central/Version20260523221257.php new file mode 100644 index 0000000..062003a --- /dev/null +++ b/httpdocs/migrations/central/Version20260523221257.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE account_user ADD archived_at DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE account_user DROP archived_at'); + } +} diff --git a/httpdocs/migrations/tenant/.gitkeep b/httpdocs/migrations/tenant/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/httpdocs/package-lock.json b/httpdocs/package-lock.json new file mode 100644 index 0000000..e54582b --- /dev/null +++ b/httpdocs/package-lock.json @@ -0,0 +1,5812 @@ +{ + "name": "html", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "license": "UNLICENSED", + "devDependencies": { + "@babel/core": "^7.17.0", + "@babel/preset-env": "^7.16.0", + "@symfony/webpack-encore": "^6.0.0", + "core-js": "^3.38.0", + "regenerator-runtime": "^0.13.9", + "sass": "^1.99.0", + "sass-loader": "^16.0.8", + "webpack": "^5.72", + "webpack-cli": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz", + "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz", + "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.4", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colordx/core": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.4.3.tgz", + "integrity": "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kocal/friendly-errors-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@kocal/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-ELJuHiZIFlynsi+hRmxZYZHrN3nF+j4wXRaIgVGVGy/oMtvm0Fmv8n+ZSWurFkWZ+wH/SV3grE+WhNP2MbfypA==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3", + "error-stack-parser": "^2.1.4", + "picocolors": "^1.1.1", + "string-width": "^4.2.3" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@symfony/webpack-encore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@symfony/webpack-encore/-/webpack-encore-6.0.0.tgz", + "integrity": "sha512-wiBvZ9QRTG5QAK9Ux4bCsbsRtzMlgzd9Eh4wSR5dOY6F3aSkgfUmxQsLKFF2lorNQinAYb4VYZtWH/9KEEt2xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kocal/friendly-errors-webpack-plugin": "^3.0.0", + "babel-loader": "^10.0.0", + "css-loader": "^7.1.0", + "css-minimizer-webpack-plugin": "^8.0.0", + "fastest-levenshtein": "^1.0.16", + "mini-css-extract-plugin": "^2.6.0", + "picocolors": "^1.1.0", + "pretty-error": "^4.0.0", + "resolve-url-loader": "^5.0.0", + "semver": "^7.3.2", + "style-loader": "^4.0.0", + "tapable": "^2.2.1", + "terser-webpack-plugin": "^5.3.0", + "tmp": "^0.2.5", + "webpack-manifest-plugin": "^5.0.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "encore": "bin/encore.js" + }, + "engines": { + "node": "^22.13.0 || >=24.0" + }, + "peerDependencies": { + "@babel/core": "^7.17.0", + "@babel/plugin-transform-react-jsx": "^7.12.11", + "@babel/preset-env": "^7.16.0", + "@babel/preset-react": "^7.9.0", + "@babel/preset-typescript": "^7.0.0", + "@symfony/stimulus-bridge": "^3.0.0 || ^4.0.0", + "@vue/babel-plugin-jsx": "^1.0.0", + "@vue/compiler-sfc": "^3.0.2", + "fork-ts-checker-webpack-plugin": "^7.0.0 || ^8.0.0 || ^9.0.0", + "handlebars": "^4.7.7", + "handlebars-loader": "^1.7.0", + "less": "^4.0.0", + "less-loader": "^12.2.0", + "postcss": "^8.3.0", + "postcss-loader": "^8.1.0", + "sass": "^1.17.0", + "sass-loader": "^16.0.1", + "stylus-loader": "^8.1.0", + "svelte": "^3.50.0 || ^4.2.2 || ^5.0.0", + "svelte-loader": "^3.1.0", + "ts-loader": "^9.0.0", + "typescript": "^5.0.0", + "vue": "^3.2.14", + "vue-loader": "^17.0.0", + "webpack": "^5.72", + "webpack-cli": "^6.0.0", + "webpack-notifier": "^1.15.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": false + }, + "@babel/plugin-transform-react-jsx": { + "optional": true + }, + "@babel/preset-env": { + "optional": false + }, + "@babel/preset-react": { + "optional": true + }, + "@babel/preset-typescript": { + "optional": true + }, + "@symfony/stimulus-bridge": { + "optional": true + }, + "@vue/babel-plugin-jsx": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "fork-ts-checker-webpack-plugin": { + "optional": true + }, + "handlebars": { + "optional": true + }, + "handlebars-loader": { + "optional": true + }, + "less": { + "optional": true + }, + "less-loader": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-loader": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-loader": { + "optional": true + }, + "stylus-loader": { + "optional": true + }, + "svelte": { + "optional": true + }, + "svelte-loader": { + "optional": true + }, + "ts-loader": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-loader": { + "optional": true + }, + "webpack": { + "optional": false + }, + "webpack-cli": { + "optional": false + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-notifier": { + "optional": true + } + } + }, + "node_modules/@symfony/webpack-encore/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-loader": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", + "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": "^18.20.0 || ^20.10.0 || >=22.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0 || ^8.0.0-beta.1", + "@rspack/core": "^1.0.0 || ^2.0.0-0", + "webpack": ">=5.61.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", + "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-9bEpzHs8gEq6/cbEj418jXL/YWjBUD2YTLLk905Npt2JODqnRITin0+So5Vx4Dp5vyi2Lpt9pp2QHzQ7fdxNrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "cssnano": "^7.0.4", + "jest-worker": "^30.0.5", + "postcss": "^8.4.40", + "schema-utils": "^4.2.0", + "serialize-javascript": "^7.0.3" + }, + "engines": { + "node": ">= 20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.9.tgz", + "integrity": "sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^7.0.17", + "lilconfig": "^3.1.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/cssnano-preset-default": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.17.tgz", + "integrity": "sha512-11qO63A+czwguQFJCaTdICvbaxn0pJzz/XghLlv+OT7WyToDxAMR0Xb3/26/l0y0hQJywwNbj/SLSQlGBHE1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.3", + "postcss-calc": "^10.1.1", + "postcss-colormin": "^7.0.10", + "postcss-convert-values": "^7.0.12", + "postcss-discard-comments": "^7.0.8", + "postcss-discard-duplicates": "^7.0.4", + "postcss-discard-empty": "^7.0.3", + "postcss-discard-overridden": "^7.0.3", + "postcss-merge-longhand": "^7.0.7", + "postcss-merge-rules": "^7.0.11", + "postcss-minify-font-values": "^7.0.3", + "postcss-minify-gradients": "^7.0.5", + "postcss-minify-params": "^7.0.9", + "postcss-minify-selectors": "^7.1.2", + "postcss-normalize-charset": "^7.0.3", + "postcss-normalize-display-values": "^7.0.3", + "postcss-normalize-positions": "^7.0.4", + "postcss-normalize-repeat-style": "^7.0.4", + "postcss-normalize-string": "^7.0.3", + "postcss-normalize-timing-functions": "^7.0.3", + "postcss-normalize-unicode": "^7.0.9", + "postcss-normalize-url": "^7.0.3", + "postcss-normalize-whitespace": "^7.0.3", + "postcss-ordered-values": "^7.0.4", + "postcss-reduce-initial": "^7.0.9", + "postcss-reduce-transforms": "^7.0.3", + "postcss-svgo": "^7.1.3", + "postcss-unique-selectors": "^7.0.7" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/cssnano-utils": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.3.tgz", + "integrity": "sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.360", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", + "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", + "integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", + "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/postcss-colormin": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.10.tgz", + "integrity": "sha512-yFr6JezOolHLta/buLE71VKPh2mXursp4saVe98/ol8ZnEWhL+racShqPKlvd/DKWLre/39B6HhcMXf7RZ3hxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colordx/core": "^5.4.3", + "browserslist": "^4.28.2", + "caniuse-api": "^3.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-convert-values": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.12.tgz", + "integrity": "sha512-xurKu5qqk4viR3Cp3p4xBR4KfnZm4w4ys6+UBwBmeuBSNkH7+DtLnYOYnOffgtE4yx8sH9S1VZ6RAAvROXzP2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-discard-comments": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.8.tgz", + "integrity": "sha512-CvvS5S9WrXblFXCEJ9nVo+4z+eA7zSC7Z88V1HEJuwlQhlFnYTIjg1xJY+BCUiG2bvICap2tXii4mP22BD108Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.4.tgz", + "integrity": "sha512-VBNn1+EuMZkeGVVtz0gRfbNGtx9IFgAsAV+E2pHtXPrp4qfGBkhTIiAuE/wrb+Y6Pakg9NewAlfTpYIFAWODtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-discard-empty": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.3.tgz", + "integrity": "sha512-M2pyjQCU+/7cMHVtL6bKTHjv0lZnPLMpicgr67Dlth7AbuV9gjVTtUqaRwn6Pp6BwSDspUzhz8SaUrRykJU5Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.3.tgz", + "integrity": "sha512-aNovXo9UsZuRNLzHJtp13lHIvinDPfiXBPePpXkSjCbgp++iU2FqE+YxvjIsg6EdyPZsASFbfu+JcBFVsErXIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.7.tgz", + "integrity": "sha512-b3mfYUxR388u5Pt0HPcVIUtUDn/k15UfTY9M+ORW+meCR6JLNxoZffiYvXyOYQoRYQNZyX/UFkMCM/mNHxe1qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.11" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-merge-rules": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.11.tgz", + "integrity": "sha512-SJUPM18g2BmPhf8BVlbwqWz4aK3pLu6u6xjfwEzra7xL6IBR10sUaiB++EzqcVfadPHrKBSMlNdP+XieykhI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.3", + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.3.tgz", + "integrity": "sha512-yilG/VOaNI74IylQvAQQxm3/wZVBkXyYUqNUAdxqwtbWUXPsbK1q8Ms0mL83v+f8YicgcyfYCRZtWACUdYajpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.5.tgz", + "integrity": "sha512-YraROyQRg3BI1+Hg8E05B/JPdnTm8EDSVu4P2BxdM+CRiOyfmou809+chGIqo6fQqwjPGQ947nbGncSjmTU1WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colordx/core": "^5.4.3", + "cssnano-utils": "^5.0.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-minify-params": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.9.tgz", + "integrity": "sha512-R8itbB8BhlpoYyBm1ou0dD+vJnQ3F6adQipR4UnkCHUwlo+S9WXJaDRg1RHjC8YVAtIdrQzSWvJl40HnGDTKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "cssnano-utils": "^5.0.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.1.2.tgz", + "integrity": "sha512-aQtrEWKwqafNlExcKHQvPGsXR2+vlUqqJtf5XsCQcgsSb5PL4wlujWBYDJuWsP4UnQX1YHDHU8qRlD+1PzTQ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-api": "^3.0.0", + "cssesc": "^3.0.0", + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.3.tgz", + "integrity": "sha512-NoBfZu8PR4c2NlmjvrqQTzCzLY79hwcSRgNQ3ZiNK0ABzf9kYKloE/jNj+/8GQY1wsm8pRRgANk6ydLH8cwo0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.3.tgz", + "integrity": "sha512-ldsCX0QIt05pKIOobZtVQ48wXJecr+czw4+e1/YjVhLMqslShgpVxgPtI2CefURR8oyVoYaU/l829MMwExDMLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.4.tgz", + "integrity": "sha512-VEvlpeGd3Ju1Hqa/oN4jaP3+ms4laYwkEL9N9u+B6k54PZjXbW1n6wI+aVprf1BQXlCYpS5+1pl/7/vHiKgARg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.4.tgz", + "integrity": "sha512-6mPKlY/8cSaDHxX502wERADarJsccwlky6yIrOapHH2ZgfoKAV94SbiTKfKEs4EEpdazuc3J72WsqeYk7hp9+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-string": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.3.tgz", + "integrity": "sha512-HnEQPUchi1eznmDKEYrKUTqrprEq97SrpUYClgUkv7V2zRODD9DFoUsYU+m9ZOetmD5ku7fEMZB/lwy8IT6xVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.3.tgz", + "integrity": "sha512-zmEzHdvpZBZu0OKlbJSfgASQvaayyAoVuWtvyr34IJ/LyS+DaOKvvR3EvFJ9RWWtNIx+CMvO125OVophaxNYew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.9.tgz", + "integrity": "sha512-DRAdWfeh/TjmhLJsw91vdiWCnUod9iwvM7xyS02/nF/sLsCR3A8l3pztrSUrWG8DSBqfX7yEk9FM0USaVJ2mSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-url": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.3.tgz", + "integrity": "sha512-CL93wmloq5qsffmFv+bw24MIRbmhHrp53qoh1LDAb/5TtjWEXI/np4xcP/Gw9oWCb2XyWnqHYLDUwiKRoJBA1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.3.tgz", + "integrity": "sha512-FdHjjn+Ht5Z2ZRjNOmeCbNq6lq09sUYKpmlF/Aq0XjVNSLTL6fmHlA/3swN2wP2caY9GV/tjSDcIIyS7aN7W0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-ordered-values": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.4.tgz", + "integrity": "sha512-nubSi49hDHQk4E8KIj+IbLY8Bg+8OcSUEhgyolgM+atnOvXjV7EjaR6bac4YGZoFyPa9mWoAF3EaYbWdFkKqVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^5.0.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.9.tgz", + "integrity": "sha512-ztTNPdIxXTxtBcG03E9u8v44M4ElXbMIRT7pf2onlquGula0Y83nKKxqM22FA/hMgkfCjN7ohevkVlaNwI8iOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.3.tgz", + "integrity": "sha512-FXsnN9ZwcZTT8Yf8cAHA8qIGUXcX6WfLd9JoYhrdDfmvsVhhfqkkv7m4AC3rwFOfz+GzkUa87OCKF9dUcicd+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.3.tgz", + "integrity": "sha512-2QfoFOYMcj8lwcVEf9WeTlkVIAm7u2QvOEhMzkQU3KUhhGX/l8hVV9EtjMv4iq3E9iI3OeeMN0YoMLbGusuigw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^4.0.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.7.tgz", + "integrity": "sha512-d+sCkaRnSefghOUdH8CMJZV9yUQhj2ojpe8Nw/lA+LV1UOfeleGkLTl6XdCFFSai9UJ+DJPb69FFuqthXYsY8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.8.tgz", + "integrity": "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/stylehacks": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.11.tgz", + "integrity": "sha512-iODNfhXVLqc5LADs+Y6Oh5wJuK5ZcHbVng8aiK3y9pjMQdc5hLrBW0eFU6FtnpNrE6PoEg/MmFTU4waotj5WNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.107.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.1.tgz", + "integrity": "sha512-mvdIWxj/H6QsfgDdH9djne3a5dYcmEmtsXGESkypaGN5jXjF/b+9KDlmTDQ2TKlFUeA2fI9Y65kihD30JOdB+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.21.4", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "webpack-sources": "^3.4.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/webpack-manifest-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.1.tgz", + "integrity": "sha512-xTlX7dC3hrASixA2inuWFMz6qHsNi6MT3Uiqw621sJjRTShtpMjbDYhPPZBwWUKdIYKIjSq9em6+uzWayf38aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "webpack": "^5.75.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz", + "integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/httpdocs/package.json b/httpdocs/package.json new file mode 100644 index 0000000..bb6f1c7 --- /dev/null +++ b/httpdocs/package.json @@ -0,0 +1,21 @@ +{ + "devDependencies": { + "@babel/core": "^7.17.0", + "@babel/preset-env": "^7.16.0", + "@symfony/webpack-encore": "^6.0.0", + "core-js": "^3.38.0", + "regenerator-runtime": "^0.13.9", + "sass": "^1.99.0", + "sass-loader": "^16.0.8", + "webpack": "^5.72", + "webpack-cli": "^6.0.0" + }, + "license": "UNLICENSED", + "private": true, + "scripts": { + "dev-server": "encore dev-server", + "dev": "encore dev", + "watch": "encore dev --watch", + "build": "encore production --progress" + } +} diff --git a/httpdocs/public/index.php b/httpdocs/public/index.php new file mode 100644 index 0000000..c0037a8 --- /dev/null +++ b/httpdocs/public/index.php @@ -0,0 +1,9 @@ +setEmail('f.eisenmenger@spawntree.de'); + $user->setFirstName('Flo'); + $user->setLastName('Eisenmenger'); + $user->setPassword($this->passwordHasher->hashPassword($user, '12spawntree345')); + $this->centralEm->persist($user); + + // ── Central: Account ────────────────────────────────────────────────── + $account = new Account(); + $account->setName('spawntree GmbH'); + $account->setSlug('spawntree'); + $this->centralEm->persist($account); + + // ── Central: AccountUser (Flo als Admin) ────────────────────────────── + $accountUser = new AccountUser(); + $accountUser->setAccount($account); + $accountUser->setUser($user); + $accountUser->setRole(AccountUser::ROLE_ADMIN); + $this->centralEm->persist($accountUser); + + $this->centralEm->flush(); + $io->text('✓ Central-DB: User, Account, AccountUser angelegt'); + + // ── Tenant-Context setzen → Middleware switcht auf db_spawntree ─────── + $this->tenantContext->setAccount($account); + + // Bestehende Connection schließen, damit Middleware greift + $this->tenantEm->getConnection()->close(); + + // ── Tenant-Schema erstellen ─────────────────────────────────────────── + $schemaTool = new SchemaTool($this->tenantEm); + $metadata = $this->tenantEm->getMetadataFactory()->getAllMetadata(); + $schemaTool->createSchema($metadata); + $io->text('✓ Tenant-Schema erstellt'); + + // ── Tenant: Leistungen ──────────────────────────────────────────────── + $serviceData = [ + ['name' => 'Frontend-Entwicklung', 'billable' => true], + ['name' => 'Software-Entwicklung', 'billable' => true], + ['name' => 'Meeting', 'billable' => true], + ['name' => 'Design', 'billable' => true], + ['name' => 'Intern', 'billable' => false], + ]; + foreach ($serviceData as $data) { + $service = new Service(); + $service->setName($data['name']); + $service->setBillable($data['billable']); + $this->tenantEm->persist($service); + } + + // ── Tenant: Kunden + Projekte ───────────────────────────────────────── + $clientsData = [ + ['name' => 'AKN GmbH', 'hourlyRate' => '95.00', 'projects' => ['Maintenance', 'Relaunch Website']], + ['name' => 'Altoelankauf.de', 'hourlyRate' => '100.00', 'projects' => ['Shop', 'SEO-Optimierung', 'Wartungsvertrag']], + ['name' => 'André Firmenich', 'hourlyRate' => '110.00', 'projects' => ['German Health Tech', 'Webumed']], + ['name' => 'Angelika Ballosch', 'hourlyRate' => '85.00', 'projects' => ['Maintenance']], + ['name' => 'Annika Teerling', 'hourlyRate' => '90.00', 'projects' => ['Website', 'Fotografie-Portfolio']], + ['name' => 'Bauunternehmen Krause', 'hourlyRate' => '100.00', 'projects' => ['Firmenwebsite', 'Stellenbörse']], + ['name' => 'Digitalagentur Nord', 'hourlyRate' => '120.00', 'projects' => ['Whitelabel CMS', 'API-Integration', 'Support']], + ['name' => 'Eventhaus Hamburg', 'hourlyRate' => '95.00', 'projects' => ['Ticketsystem', 'Landingpages']], + ['name' => 'Kanzlei Meier & Partner', 'hourlyRate' => '100.00', 'projects' => ['Kanzleiwebsite']], + ['name' => 'Spawntree (intern)', 'hourlyRate' => null, 'projects' => ['Futbase', 'Timetracker', 'Akquise']], + ]; + foreach ($clientsData as $data) { + $client = new Client(); + $client->setName($data['name']); + $client->setHourlyRate($data['hourlyRate']); + $this->tenantEm->persist($client); + + foreach ($data['projects'] as $projectName) { + $project = new Project(); + $project->setName($projectName); + $project->setClient($client); + $this->tenantEm->persist($project); + } + } + + $this->tenantEm->flush(); + $io->text('✓ Tenant-DB: Leistungen, Kunden, Projekte angelegt'); + + // ══ Zweiter Tenant: Nova-Sign ══════════════════════════════════════════ + + // ── Central: User ───────────────────────────────────────────────────── + $user2 = new User(); + $user2->setEmail('dirktietze@nova-sign.de'); + $user2->setFirstName('Dirk'); + $user2->setLastName('Tietze'); + $user2->setPassword($this->passwordHasher->hashPassword($user2, '12spawntree345')); + $this->centralEm->persist($user2); + + // ── Central: Account ────────────────────────────────────────────────── + $account2 = new Account(); + $account2->setName('Nova-Sign'); + $account2->setSlug('nova-sign'); + $this->centralEm->persist($account2); + + // ── Central: AccountUser ────────────────────────────────────────────── + $accountUser2 = new AccountUser(); + $accountUser2->setAccount($account2); + $accountUser2->setUser($user2); + $accountUser2->setRole(AccountUser::ROLE_ADMIN); + $this->centralEm->persist($accountUser2); + + $this->centralEm->flush(); + $io->text('✓ Central-DB: User, Account, AccountUser (Nova-Sign) angelegt'); + + // ── Tenant-Context umschalten auf db_nova_sign ──────────────────────── + $this->tenantContext->setAccount($account2); + $this->tenantEm->clear(); + $this->tenantEm->getConnection()->close(); + + // ── Tenant-Schema erstellen ─────────────────────────────────────────── + $schemaTool2 = new SchemaTool($this->tenantEm); + $metadata2 = $this->tenantEm->getMetadataFactory()->getAllMetadata(); + $schemaTool2->createSchema($metadata2); + $io->text('✓ Tenant-Schema (Nova-Sign) erstellt'); + + // ── Tenant: Leistungen ──────────────────────────────────────────────── + $serviceData2 = [ + ['name' => 'Beratung', 'billable' => true], + ['name' => 'Projektmanagement', 'billable' => true], + ['name' => 'Design', 'billable' => true], + ['name' => 'Produktion', 'billable' => true], + ['name' => 'Intern', 'billable' => false], + ]; + foreach ($serviceData2 as $data) { + $service = new Service(); + $service->setName($data['name']); + $service->setBillable($data['billable']); + $this->tenantEm->persist($service); + } + + // ── Tenant: Kunden + Projekte ───────────────────────────────────────── + $clientsData2 = [ + ['name' => 'Messe Stuttgart', 'hourlyRate' => '110.00', 'projects' => ['Messestand 2025', 'Leitsystem']], + ['name' => 'Autohaus Brenner', 'hourlyRate' => '95.00', 'projects' => ['Showroom-Beschriftung']], + ['name' => 'Hotel Kronsberg', 'hourlyRate' => '100.00', 'projects' => ['Wegeleitsystem', 'Zimmerbeschilderung']], + ['name' => 'Klinik am See', 'hourlyRate' => '105.00', 'projects' => ['Orientierungssystem']], + ['name' => 'Nova-Sign (intern)', 'hourlyRate' => null, 'projects' => ['Website', 'Buchhaltung']], + ]; + foreach ($clientsData2 as $data) { + $client = new Client(); + $client->setName($data['name']); + $client->setHourlyRate($data['hourlyRate']); + $this->tenantEm->persist($client); + + foreach ($data['projects'] as $projectName) { + $project = new Project(); + $project->setName($projectName); + $project->setClient($client); + $this->tenantEm->persist($project); + } + } + + $this->tenantEm->flush(); + $io->text('✓ Tenant-DB Nova-Sign: Leistungen, Kunden, Projekte angelegt'); + + $io->success('Seeding abgeschlossen. Login: f.eisenmenger@spawntree.de / 12spawntree345'); + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/httpdocs/src/Controller/.gitignore b/httpdocs/src/Controller/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/httpdocs/src/Controller/AccountController.php b/httpdocs/src/Controller/AccountController.php new file mode 100644 index 0000000..2d6dfcc --- /dev/null +++ b/httpdocs/src/Controller/AccountController.php @@ -0,0 +1,125 @@ +tenantContext->getAccount(); + $user = $this->getUser(); + $accountUser = $this->accountUserRepo->findOneBy(['account' => $account, 'user' => $user]); + $isAdmin = $accountUser?->isAdmin() ?? false; + + $tab = $request->query->get('tab', $isAdmin ? 'account' : 'user'); + if (!$isAdmin && $tab === 'account') { + $tab = 'user'; + } + + return $this->render('account/index.html.twig', [ + 'account' => $account, + 'user' => $user, + 'isAdmin' => $isAdmin, + 'tab' => $tab, + 'intervalOptions' => [ + 1 => 'Minuten', + 15 => 'Viertelstunde', + 30 => 'Halbe Stunde', + 60 => 'Stunde', + ], + ]); + } + + #[Route('/api/account', name: 'api_account_update', methods: ['PATCH'])] + public function updateAccount(Request $request): JsonResponse + { + $account = $this->tenantContext->getAccount(); + $user = $this->getUser(); + $accountUser = $this->accountUserRepo->findOneBy(['account' => $account, 'user' => $user]); + + if (!$accountUser?->isAdmin()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + + $data = json_decode($request->getContent(), true) ?? []; + + if (!empty($data['name'])) { + $account->setName(trim($data['name'])); + } + + if (isset($data['trackingInterval'])) { + $interval = (int) $data['trackingInterval']; + if (in_array($interval, [1, 15, 30, 60], true)) { + $account->setTrackingInterval($interval); + } + } + + $this->em->flush(); + + return $this->json(['ok' => true, 'name' => $account->getName()]); + } + + #[Route('/api/account/user', name: 'api_account_user_update', methods: ['PATCH'])] + public function updateUser(Request $request): JsonResponse + { + /** @var User $user */ + $user = $this->getUser(); + $data = json_decode($request->getContent(), true) ?? []; + + if (!empty($data['firstName'])) { + $user->setFirstName(trim($data['firstName'])); + } + if (!empty($data['lastName'])) { + $user->setLastName(trim($data['lastName'])); + } + + if (!empty($data['email'])) { + $newEmail = trim($data['email']); + if ($newEmail !== $user->getEmail()) { + $existing = $this->userRepo->findOneBy(['email' => $newEmail]); + if ($existing !== null && $existing->getId() !== $user->getId()) { + return $this->json(['error' => 'Diese E-Mail-Adresse wird bereits verwendet.'], 409); + } + $user->setEmail($newEmail); + } + } + + if (!empty($data['newPassword'])) { + if (empty($data['currentPassword'])) { + return $this->json(['error' => 'Aktuelles Passwort ist erforderlich.'], 400); + } + if (!$this->passwordHasher->isPasswordValid($user, $data['currentPassword'])) { + return $this->json(['error' => 'Das aktuelle Passwort ist falsch.'], 400); + } + if (strlen($data['newPassword']) < 8) { + return $this->json(['error' => 'Das neue Passwort muss mindestens 8 Zeichen haben.'], 400); + } + $user->setPassword($this->passwordHasher->hashPassword($user, $data['newPassword'])); + } + + $this->em->flush(); + + return $this->json(['ok' => true]); + } +} \ No newline at end of file diff --git a/httpdocs/src/Controller/ClientController.php b/httpdocs/src/Controller/ClientController.php new file mode 100644 index 0000000..71bce9e --- /dev/null +++ b/httpdocs/src/Controller/ClientController.php @@ -0,0 +1,143 @@ +roleHelper->isTracker()) { + throw $this->createAccessDeniedException(); + } + return $this->render('client/index.html.twig', [ + 'clients' => $this->clientRepo->findAllOrderedByName(), + ]); + } + + #[Route('/api/clients', name: 'api_client_create', methods: ['POST'])] + public function create(Request $request): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $data = json_decode($request->getContent(), true); + + if (empty($data['name'])) { + return $this->json(['error' => 'Name ist erforderlich'], 400); + } + + $client = new Client(); + $client->setName(trim($data['name'])); + $client->setHourlyRate(!empty($data['hourlyRate']) ? $data['hourlyRate'] : null); + $client->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->em->persist($client); + $this->em->flush(); + + return $this->json($this->clientToArray($client), 201); + } + + #[Route('/api/clients/{id}', name: 'api_client_update', methods: ['PATCH'])] + public function update(int $id, Request $request): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $client = $this->clientRepo->find($id); + if (!$client) return $this->json(['error' => 'Nicht gefunden'], 404); + + $data = json_decode($request->getContent(), true); + + if (empty($data['name'])) { + return $this->json(['error' => 'Name ist erforderlich'], 400); + } + + $client->setName(trim($data['name'])); + $client->setHourlyRate(!empty($data['hourlyRate']) ? $data['hourlyRate'] : null); + $client->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->em->flush(); + + return $this->json($this->clientToArray($client)); + } + + #[Route('/api/clients/{id}', name: 'api_client_delete', methods: ['DELETE'])] + public function delete(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $client = $this->clientRepo->find($id); + if (!$client) return $this->json(['error' => 'Nicht gefunden'], 404); + + if ($this->timeEntryRepo->countByClient($client) > 0) { + return $this->json(['error' => 'has_dependencies', 'canArchive' => true], 409); + } + + $this->em->remove($client); + $this->em->flush(); + + return $this->json(['success' => true]); + } + + #[Route('/api/clients/{id}/archive', name: 'api_client_archive', methods: ['PATCH'])] + public function archive(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $client = $this->clientRepo->find($id); + if (!$client) return $this->json(['error' => 'Nicht gefunden'], 404); + + $client->setArchivedAt(new \DateTimeImmutable()); + $this->em->flush(); + + return $this->json($this->clientToArray($client)); + } + + #[Route('/api/clients/{id}/unarchive', name: 'api_client_unarchive', methods: ['PATCH'])] + public function unarchive(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $client = $this->clientRepo->find($id); + if (!$client) return $this->json(['error' => 'Nicht gefunden'], 404); + + $client->setArchivedAt(null); + $this->em->flush(); + + return $this->json($this->clientToArray($client)); + } + + private function clientToArray(Client $client): array + { + return [ + 'id' => $client->getId(), + 'name' => $client->getName(), + 'hourlyRate' => $client->getHourlyRate(), + 'note' => $client->getNote(), + 'projectCount' => $client->getProjects()->count(), + 'archived' => $client->isArchived(), + ]; + } +} diff --git a/httpdocs/src/Controller/HomeController.php b/httpdocs/src/Controller/HomeController.php new file mode 100644 index 0000000..6fd46e4 --- /dev/null +++ b/httpdocs/src/Controller/HomeController.php @@ -0,0 +1,26 @@ +tenantContext->hasAccount()) { + if ($this->getUser()) { + return $this->redirectToRoute('timetracking_week'); + } + return $this->redirectToRoute('app_login'); + } + + return $this->render('home/index.html.twig'); + } +} \ No newline at end of file diff --git a/httpdocs/src/Controller/InviteController.php b/httpdocs/src/Controller/InviteController.php new file mode 100644 index 0000000..f794528 --- /dev/null +++ b/httpdocs/src/Controller/InviteController.php @@ -0,0 +1,101 @@ +inviteTokenRepo->findOneBy(['token' => $token]); + + if ($invite === null) { + return $this->render('invite/error.html.twig', [ + 'error' => 'Dieser Einladungslink ist ungültig.', + ]); + } + + if ($invite->isExpired()) { + return $this->render('invite/error.html.twig', [ + 'error' => 'Dieser Einladungslink ist abgelaufen (gültig 7 Tage).', + ]); + } + + // Account-Kontext prüfen (Sicherheit: Link muss auf richtigem Subdomain geöffnet werden) + $account = $this->tenantContext->getAccount(); + if ($account === null || $account->getId() !== $invite->getAccount()?->getId()) { + return $this->render('invite/error.html.twig', [ + 'error' => 'Dieser Einladungslink gehört zu einem anderen Account.', + ]); + } + + $error = null; + + if ($request->isMethod('POST')) { + $password = $request->request->get('password', ''); + $passwordRepeat = $request->request->get('passwordRepeat', ''); + + if (strlen($password) < 8) { + $error = 'Das Passwort muss mindestens 8 Zeichen haben.'; + } elseif ($password !== $passwordRepeat) { + $error = 'Die Passwörter stimmen nicht überein.'; + } else { + // User anlegen (oder existierenden finden, falls E-Mail schon vorhanden) + $user = $this->userRepo->findOneBy(['email' => $invite->getEmail()]); + + if ($user === null) { + $user = new User(); + $user->setEmail($invite->getEmail()); + $user->setFirstName($invite->getFirstName()); + $user->setLastName($invite->getLastName()); + $this->em->persist($user); + } + + $user->setPassword($this->passwordHasher->hashPassword($user, $password)); + + // AccountUser anlegen + $accountUser = new AccountUser(); + $accountUser->setAccount($invite->getAccount()); + $accountUser->setUser($user); + $accountUser->setRole($invite->getRole()); + $this->em->persist($accountUser); + + // Token löschen + $this->em->remove($invite); + $this->em->flush(); + + // Direkt einloggen + $this->security->login($user, 'form_login', 'main'); + + return $this->redirectToRoute('timetracking_week'); + } + } + + return $this->render('invite/set_password.html.twig', [ + 'invite' => $invite, + 'error' => $error, + ]); + } +} \ No newline at end of file diff --git a/httpdocs/src/Controller/ProjectController.php b/httpdocs/src/Controller/ProjectController.php new file mode 100644 index 0000000..f17bb35 --- /dev/null +++ b/httpdocs/src/Controller/ProjectController.php @@ -0,0 +1,146 @@ +roleHelper->isTracker()) { + throw $this->createAccessDeniedException(); + } + return $this->render('project/index.html.twig', [ + 'projects' => $this->projectRepo->findAllWithClient(), + 'clients' => $this->clientRepo->findAllOrderedByName(), + ]); + } + + #[Route('/api/projects', name: 'api_project_create', methods: ['POST'])] + public function create(Request $request): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $data = json_decode($request->getContent(), true); + $client = $this->clientRepo->find($data['clientId'] ?? 0); + + if (empty($data['name'])) return $this->json(['error' => 'Name ist erforderlich'], 400); + if (!$client) return $this->json(['error' => 'Kunde nicht gefunden'], 400); + + $project = new Project(); + $project->setName(trim($data['name'])); + $project->setClient($client); + $project->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->em->persist($project); + $this->em->flush(); + + return $this->json($this->projectToArray($project), 201); + } + + #[Route('/api/projects/{id}', name: 'api_project_update', methods: ['PATCH'])] + public function update(int $id, Request $request): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $project = $this->projectRepo->find($id); + if (!$project) return $this->json(['error' => 'Nicht gefunden'], 404); + + $data = json_decode($request->getContent(), true); + $client = $this->clientRepo->find($data['clientId'] ?? 0); + + if (empty($data['name'])) return $this->json(['error' => 'Name ist erforderlich'], 400); + if (!$client) return $this->json(['error' => 'Kunde nicht gefunden'], 400); + + $project->setName(trim($data['name'])); + $project->setClient($client); + $project->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->em->flush(); + + return $this->json($this->projectToArray($project)); + } + + #[Route('/api/projects/{id}', name: 'api_project_delete', methods: ['DELETE'])] + public function delete(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $project = $this->projectRepo->find($id); + if (!$project) return $this->json(['error' => 'Nicht gefunden'], 404); + + if ($this->timeEntryRepo->countByProject($project) > 0) { + return $this->json(['error' => 'has_dependencies', 'canArchive' => true], 409); + } + + $this->em->remove($project); + $this->em->flush(); + + return $this->json(['success' => true]); + } + + #[Route('/api/projects/{id}/archive', name: 'api_project_archive', methods: ['PATCH'])] + public function archive(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $project = $this->projectRepo->find($id); + if (!$project) return $this->json(['error' => 'Nicht gefunden'], 404); + + $project->setArchivedAt(new \DateTimeImmutable()); + $this->em->flush(); + + return $this->json($this->projectToArray($project)); + } + + #[Route('/api/projects/{id}/unarchive', name: 'api_project_unarchive', methods: ['PATCH'])] + public function unarchive(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $project = $this->projectRepo->find($id); + if (!$project) return $this->json(['error' => 'Nicht gefunden'], 404); + + $project->setArchivedAt(null); + $this->em->flush(); + + return $this->json($this->projectToArray($project)); + } + + private function projectToArray(Project $project): array + { + return [ + 'id' => $project->getId(), + 'name' => $project->getName(), + 'clientId' => $project->getClient()->getId(), + 'clientName' => $project->getClient()->getName(), + 'note' => $project->getNote(), + 'archived' => $project->isArchived(), + ]; + } +} diff --git a/httpdocs/src/Controller/RegistrationController.php b/httpdocs/src/Controller/RegistrationController.php new file mode 100644 index 0000000..d5afb92 --- /dev/null +++ b/httpdocs/src/Controller/RegistrationController.php @@ -0,0 +1,103 @@ +render('registration/register.html.twig', [ + 'appDomain' => $this->appDomain, + ]); + } + + /** + * Live-Vorschau des Slugs während der User tippt. + */ + #[Route('/api/register/preview-slug', name: 'api_register_preview_slug', methods: ['POST'])] + public function previewSlug(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + $companyName = trim($data['companyName'] ?? ''); + + if ($companyName === '') { + return $this->json(['slug' => '']); + } + + try { + $slug = $this->slugGenerator->previewFromCompanyName($companyName); + return $this->json(['slug' => $slug]); + } catch (\Throwable) { + return $this->json(['slug' => '']); + } + } + + #[Route('/api/register', name: 'api_register', methods: ['POST'])] + public function submit(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true) ?? []; + + $companyName = trim($data['companyName'] ?? ''); + $email = trim($data['email'] ?? ''); + $firstName = trim($data['firstName'] ?? ''); + $lastName = trim($data['lastName'] ?? ''); + $password = $data['password'] ?? ''; + $passwordRepeat = $data['passwordRepeat'] ?? ''; + + $errors = []; + if ($companyName === '') { $errors[] = 'Firmenname ist erforderlich.'; } + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Keine gültige E-Mail-Adresse.'; } + if ($firstName === '') { $errors[] = 'Vorname ist erforderlich.'; } + if ($lastName === '') { $errors[] = 'Nachname ist erforderlich.'; } + if (strlen($password) < 8) { $errors[] = 'Passwort muss mindestens 8 Zeichen lang sein.'; } + if ($password !== $passwordRepeat) { $errors[] = 'Passwörter stimmen nicht überein.'; } + + if (!empty($errors)) { + return $this->json(['errors' => $errors], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + try { + $this->registrationService->startRegistration( + $companyName, $email, $firstName, $lastName, $password, + ); + return $this->json(['success' => true]); + } catch (\DomainException $e) { + return $this->json(['errors' => [$e->getMessage()]], Response::HTTP_UNPROCESSABLE_ENTITY); + } catch (\Throwable $e) { + return $this->json(['errors' => ['Ein Fehler ist aufgetreten. Bitte versuche es erneut.']], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + #[Route('/verify/{token}', name: 'app_verify')] + public function verify(string $token): Response + { + try { + $account = $this->registrationService->confirm($token); + $redirectUrl = 'https://' . $account->getSlug() . '.' . $this->appDomain; + + return $this->render('registration/confirmed.html.twig', [ + 'account' => $account, + 'redirectUrl' => $redirectUrl, + ]); + } catch (\InvalidArgumentException $e) { + return $this->render('registration/confirm_error.html.twig', [ + 'error' => $e->getMessage(), + ]); + } + } +} \ No newline at end of file diff --git a/httpdocs/src/Controller/SecurityController.php b/httpdocs/src/Controller/SecurityController.php new file mode 100644 index 0000000..f2141f1 --- /dev/null +++ b/httpdocs/src/Controller/SecurityController.php @@ -0,0 +1,38 @@ +getUser()) { + return $this->redirectToRoute('timetracking_week'); + } + + if (!$this->tenantContext->hasAccount()) { + return $this->redirectToRoute('app_home'); + } + + return $this->render('security/login.html.twig', [ + 'lastUsername' => $authenticationUtils->getLastUsername(), + 'error' => $authenticationUtils->getLastAuthenticationError(), + 'accountName' => $this->tenantContext->getAccount()?->getName() ?? 'spawntree', + ]); + } + + #[Route('/logout', name: 'app_logout')] + public function logout(): void + { + throw new \LogicException('This method should never be reached.'); + } +} \ No newline at end of file diff --git a/httpdocs/src/Controller/ServiceController.php b/httpdocs/src/Controller/ServiceController.php new file mode 100644 index 0000000..71916e5 --- /dev/null +++ b/httpdocs/src/Controller/ServiceController.php @@ -0,0 +1,138 @@ +roleHelper->isTracker()) { + throw $this->createAccessDeniedException(); + } + return $this->render('service/index.html.twig', [ + 'services' => $this->serviceRepo->findAllOrderedByBillable(), + ]); + } + + #[Route('/api/services', name: 'api_service_create', methods: ['POST'])] + public function create(Request $request): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $data = json_decode($request->getContent(), true); + + if (empty($data['name'])) return $this->json(['error' => 'Name ist erforderlich'], 400); + + $service = new Service(); + $service->setName(trim($data['name'])); + $service->setBillable((bool) ($data['billable'] ?? true)); + $service->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->em->persist($service); + $this->em->flush(); + + return $this->json($this->serviceToArray($service), 201); + } + + #[Route('/api/services/{id}', name: 'api_service_update', methods: ['PATCH'])] + public function update(int $id, Request $request): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $service = $this->serviceRepo->find($id); + if (!$service) return $this->json(['error' => 'Nicht gefunden'], 404); + + $data = json_decode($request->getContent(), true); + + if (empty($data['name'])) return $this->json(['error' => 'Name ist erforderlich'], 400); + + $service->setName(trim($data['name'])); + $service->setBillable((bool) ($data['billable'] ?? true)); + $service->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->em->flush(); + + return $this->json($this->serviceToArray($service)); + } + + #[Route('/api/services/{id}', name: 'api_service_delete', methods: ['DELETE'])] + public function delete(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $service = $this->serviceRepo->find($id); + if (!$service) return $this->json(['error' => 'Nicht gefunden'], 404); + + if ($this->timeEntryRepo->countByService($service) > 0) { + return $this->json(['error' => 'has_dependencies', 'canArchive' => true], 409); + } + + $this->em->remove($service); + $this->em->flush(); + + return $this->json(['success' => true]); + } + + #[Route('/api/services/{id}/archive', name: 'api_service_archive', methods: ['PATCH'])] + public function archive(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $service = $this->serviceRepo->find($id); + if (!$service) return $this->json(['error' => 'Nicht gefunden'], 404); + + $service->setArchivedAt(new \DateTimeImmutable()); + $this->em->flush(); + + return $this->json($this->serviceToArray($service)); + } + + #[Route('/api/services/{id}/unarchive', name: 'api_service_unarchive', methods: ['PATCH'])] + public function unarchive(int $id): JsonResponse + { + if ($this->roleHelper->isTracker()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + $service = $this->serviceRepo->find($id); + if (!$service) return $this->json(['error' => 'Nicht gefunden'], 404); + + $service->setArchivedAt(null); + $this->em->flush(); + + return $this->json($this->serviceToArray($service)); + } + + private function serviceToArray(Service $service): array + { + return [ + 'id' => $service->getId(), + 'name' => $service->getName(), + 'billable' => $service->isBillable(), + 'note' => $service->getNote(), + 'archived' => $service->isArchived(), + ]; + } +} diff --git a/httpdocs/src/Controller/TeamController.php b/httpdocs/src/Controller/TeamController.php new file mode 100644 index 0000000..dd1a481 --- /dev/null +++ b/httpdocs/src/Controller/TeamController.php @@ -0,0 +1,313 @@ +roleHelper->isAdmin()) { + throw $this->createAccessDeniedException(); + } + + $account = $this->tenantContext->getAccount(); + $allUsers = $this->accountUserRepo->findBy(['account' => $account]); + $pendingInvites = $this->inviteTokenRepo->findBy(['account' => $account]); + + $activeUsers = array_values(array_filter($allUsers, fn($au) => !$au->isArchived())); + $archivedUsers = array_values(array_filter($allUsers, fn($au) => $au->isArchived())); + + return $this->render('team/index.html.twig', [ + 'activeUsers' => $activeUsers, + 'archivedUsers' => $archivedUsers, + 'pendingInvites' => $pendingInvites, + 'currentUserId' => $this->getUser()?->getId(), + ]); + } + + #[Route('/api/team/invite', name: 'api_team_invite', methods: ['POST'])] + public function invite(Request $request): JsonResponse + { + if (!$this->roleHelper->isAdmin()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + + $data = json_decode($request->getContent(), true) ?? []; + $email = trim($data['email'] ?? ''); + $firstName = trim($data['firstName'] ?? ''); + $lastName = trim($data['lastName'] ?? ''); + $role = $data['role'] ?? AccountUser::ROLE_MEMBER; + + $errors = []; + if ($email === '') { $errors[] = 'E-Mail ist erforderlich.'; } + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Keine gültige E-Mail-Adresse.'; } + if ($firstName === '') { $errors[] = 'Vorname ist erforderlich.'; } + if ($lastName === '') { $errors[] = 'Nachname ist erforderlich.'; } + if (!in_array($role, [AccountUser::ROLE_ADMIN, AccountUser::ROLE_MEMBER, AccountUser::ROLE_TRACKER], true)) { + $errors[] = 'Ungültige Rolle.'; + } + + if (!empty($errors)) { + return $this->json(['errors' => $errors], 422); + } + + $account = $this->tenantContext->getAccount(); + + $existingUser = $this->userRepo->findOneBy(['email' => $email]); + if ($existingUser !== null) { + $alreadyMember = $this->accountUserRepo->findOneBy([ + 'account' => $account, + 'user' => $existingUser, + ]); + if ($alreadyMember !== null) { + return $this->json(['errors' => ['Diese Person ist bereits Mitglied dieses Accounts.']], 409); + } + } + + $existing = $this->inviteTokenRepo->findOneBy(['account' => $account, 'email' => $email]); + if ($existing !== null) { + $this->em->remove($existing); + $this->em->flush(); + } + + $invite = new InviteToken(); + $invite->setToken(bin2hex(random_bytes(32))); + $invite->setAccount($account); + $invite->setEmail($email); + $invite->setFirstName($firstName); + $invite->setLastName($lastName); + $invite->setRole($role); + + $this->em->persist($invite); + $this->em->flush(); + + $this->sendInviteMail($invite); + + return $this->json(['ok' => true]); + } + + #[Route('/api/team/{id}/archive', name: 'api_team_archive', methods: ['PATCH'])] + public function archive(int $id): JsonResponse + { + if (!$this->roleHelper->isAdmin()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + + $account = $this->tenantContext->getAccount(); + $accountUser = $this->accountUserRepo->find($id); + + if ($accountUser === null || $accountUser->getAccount()?->getId() !== $account?->getId()) { + return $this->json(['error' => 'Nicht gefunden'], 404); + } + + if ($accountUser->getUser() === $this->getUser()) { + return $this->json(['error' => 'Du kannst dich nicht selbst archivieren.'], 400); + } + + $accountUser->setArchivedAt(new \DateTimeImmutable()); + $this->em->flush(); + + return $this->json(['ok' => true]); + } + + #[Route('/api/team/{id}/unarchive', name: 'api_team_unarchive', methods: ['PATCH'])] + public function unarchive(int $id): JsonResponse + { + if (!$this->roleHelper->isAdmin()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + + $account = $this->tenantContext->getAccount(); + $accountUser = $this->accountUserRepo->find($id); + + if ($accountUser === null || $accountUser->getAccount()?->getId() !== $account?->getId()) { + return $this->json(['error' => 'Nicht gefunden'], 404); + } + + $accountUser->setArchivedAt(null); + $this->em->flush(); + + return $this->json(['ok' => true]); + } + + #[Route('/api/team/{id}', name: 'api_team_edit', methods: ['PATCH'])] + public function edit(int $id, Request $request): JsonResponse + { + if (!$this->roleHelper->isAdmin()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + + $account = $this->tenantContext->getAccount(); + $accountUser = $this->accountUserRepo->find($id); + + if ($accountUser === null || $accountUser->getAccount()?->getId() !== $account?->getId()) { + return $this->json(['error' => 'Nicht gefunden'], 404); + } + + $data = json_decode($request->getContent(), true) ?? []; + $firstName = trim($data['firstName'] ?? ''); + $lastName = trim($data['lastName'] ?? ''); + $email = trim($data['email'] ?? ''); + $note = $data['note'] !== '' ? ($data['note'] ?? null) : null; + $role = $data['role'] ?? null; + + $errors = []; + if ($firstName === '') { $errors[] = 'Vorname ist erforderlich.'; } + if ($lastName === '') { $errors[] = 'Nachname ist erforderlich.'; } + if ($email === '') { $errors[] = 'E-Mail ist erforderlich.'; } + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Keine gültige E-Mail-Adresse.'; } + if ($role !== null && !in_array($role, [AccountUser::ROLE_ADMIN, AccountUser::ROLE_MEMBER, AccountUser::ROLE_TRACKER], true)) { + $errors[] = 'Ungültige Rolle.'; + } + + if (!empty($errors)) { + return $this->json(['errors' => $errors], 422); + } + + $user = $accountUser->getUser(); + + // E-Mail-Änderung: Duplikat prüfen + if ($email !== $user->getEmail()) { + $existing = $this->userRepo->findOneBy(['email' => $email]); + if ($existing !== null) { + return $this->json(['errors' => ['Diese E-Mail-Adresse wird bereits verwendet.']], 409); + } + } + + // Eigene Rolle: Admin darf sich nicht selbst degradieren + $isSelf = ($user === $this->getUser()); + if ($isSelf && $accountUser->isAdmin() && $role !== null && $role !== AccountUser::ROLE_ADMIN) { + return $this->json(['errors' => ['Du kannst deine eigene Administratoren-Rolle nicht ändern.']], 400); + } + + $user->setFirstName($firstName); + $user->setLastName($lastName); + $user->setEmail($email); + $user->setNote($note !== '' ? $note : null); + + if ($role !== null && !($isSelf && $accountUser->isAdmin())) { + $accountUser->setRole($role); + } + + $this->em->flush(); + + return $this->json($this->accountUserToArray($accountUser)); + } + + #[Route('/api/team/{id}', name: 'api_team_delete', methods: ['DELETE'])] + public function delete(int $id): JsonResponse + { + if (!$this->roleHelper->isAdmin()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + + $account = $this->tenantContext->getAccount(); + $accountUser = $this->accountUserRepo->find($id); + + if ($accountUser === null || $accountUser->getAccount()?->getId() !== $account?->getId()) { + return $this->json(['error' => 'Nicht gefunden'], 404); + } + + if ($accountUser->getUser() === $this->getUser()) { + return $this->json(['error' => 'Du kannst dich nicht selbst entfernen.'], 400); + } + + $userId = $accountUser->getUser()->getId(); + if ($this->timeEntryRepo->countByUserId($userId) > 0) { + return $this->json(['error' => 'has_dependencies', 'canArchive' => true], 409); + } + + $this->em->remove($accountUser); + $this->em->flush(); + + return $this->json(['ok' => true]); + } + + #[Route('/api/team/invite/{id}', name: 'api_team_invite_delete', methods: ['DELETE'])] + public function deleteInvite(int $id): JsonResponse + { + if (!$this->roleHelper->isAdmin()) { + return $this->json(['error' => 'Zugriff verweigert'], 403); + } + + $account = $this->tenantContext->getAccount(); + $invite = $this->inviteTokenRepo->find($id); + + if ($invite === null || $invite->getAccount()?->getId() !== $account?->getId()) { + return $this->json(['error' => 'Nicht gefunden'], 404); + } + + $this->em->remove($invite); + $this->em->flush(); + + return $this->json(['ok' => true]); + } + + private function accountUserToArray(AccountUser $au): array + { + return [ + 'id' => $au->getId(), + 'firstName' => $au->getUser()->getFirstName(), + 'lastName' => $au->getUser()->getLastName(), + 'fullName' => $au->getUser()->getFullName(), + 'email' => $au->getUser()->getEmail(), + 'note' => $au->getUser()->getNote(), + 'role' => $au->getRole(), + 'roleLabel' => $au->getRoleLabel(), + ]; + } + + private function sendInviteMail(InviteToken $invite): void + { + $inviteUrl = 'https://' + . $invite->getAccount()->getSlug() + . '.' + . $this->appDomain + . $this->urlGenerator->generate('app_invite', ['token' => $invite->getToken()]); + + $email = (new TemplatedEmail()) + ->to(new Address($invite->getEmail(), $invite->getFirstName() . ' ' . $invite->getLastName())) + ->subject('Einladung zu ' . $invite->getAccount()->getName()) + ->htmlTemplate('email/team_invite.html.twig') + ->context([ + 'invite' => $invite, + 'inviteUrl' => $inviteUrl, + ]); + + $this->mailer->send($email); + } +} \ No newline at end of file diff --git a/httpdocs/src/Controller/TimeTrackingController.php b/httpdocs/src/Controller/TimeTrackingController.php new file mode 100644 index 0000000..565ba3b --- /dev/null +++ b/httpdocs/src/Controller/TimeTrackingController.php @@ -0,0 +1,265 @@ +getUser(); + $entries = $this->timeEntryRepo->findByDateAndUserId($activeDate, $user->getId()); + $totalMin = $this->timeEntryRepo->sumDurationByDateAndUserId($activeDate, $user->getId()); + + $monday = $activeDate->modify('monday this week'); + $prevMonday = $monday->modify('-7 days'); + $nextMonday = $monday->modify('+7 days'); + + return $this->render('timetracking/week.html.twig', [ + 'currentDate' => $activeDate, + 'today' => $today, + 'todayStr' => $today->format('Y-m-d'), + 'tomorrowStr' => $today->modify('+1 day')->format('Y-m-d'), + 'yesterdayStr' => $today->modify('-1 day')->format('Y-m-d'), + 'currentWeekNumber' => (int) $activeDate->format('W'), + 'weekDays' => $this->buildWeekDays($activeDate, $today), + 'prevWeekUrl' => $this->generateUrl('timetracking_week_date', ['date' => $prevMonday->format('Y-m-d')]), + 'nextWeekUrl' => $this->generateUrl('timetracking_week_date', ['date' => $nextMonday->format('Y-m-d')]), + 'timeEntries' => $entries, + 'totalDuration' => $this->formatMinutes($totalMin), + 'projects' => $this->projectRepo->findAllWithClient(), + 'services' => $this->serviceRepo->findAllOrderedByBillable(), + 'greeting' => $this->getGreeting(), + 'firstName' => $user->getFirstName(), + 'trackingInterval' => $this->tenantContext->getAccount()?->getTrackingInterval() ?? 1, + ]); + } + + // ── API: Einträge für einen Tag laden ───────────────────────────────────── + + #[Route('/api/entries', name: 'api_entries_list', methods: ['GET'])] + public function apiList(Request $request): JsonResponse + { + $tz = new \DateTimeZone('Europe/Berlin'); + $date = new \DateTimeImmutable($request->query->get('date', 'today'), $tz); + + /** @var User $user */ + $user = $this->getUser(); + $entries = $this->timeEntryRepo->findByDateAndUserId($date, $user->getId()); + $totalMin = $this->timeEntryRepo->sumDurationByDateAndUserId($date, $user->getId()); + + return $this->json([ + 'entries' => array_map(fn(TimeEntry $e) => $e->toArray(), $entries), + 'totalDuration' => $this->formatMinutes($totalMin), + ]); + } + + // ── API: Eintrag erstellen ──────────────────────────────────────────────── + + #[Route('/api/entries', name: 'api_entries_create', methods: ['POST'])] + public function apiCreate(Request $request): JsonResponse + { + /** @var User $user */ + $user = $this->getUser(); + $data = json_decode($request->getContent(), true); + $project = $this->projectRepo->find($data['projectId'] ?? 0); + + if (!$project) { + return $this->json(['error' => 'Projekt nicht gefunden'], 400); + } + + $tz = new \DateTimeZone('Europe/Berlin'); + $date = new \DateTimeImmutable($data['date'] ?? 'today', $tz); + + $service = null; + if (!empty($data['serviceId'])) { + $service = $this->serviceRepo->find($data['serviceId']); + } + + $entry = new TimeEntry(); + $entry->setUserId($user->getId()); + $entry->setProject($project); + $entry->setService($service); + $entry->setDate($date); + $entry->setDuration($this->parseDuration($data['duration'] ?? '0')); + $entry->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->tenantEm->persist($entry); + $this->tenantEm->flush(); + + $totalMin = $this->timeEntryRepo->sumDurationByDateAndUserId($date, $user->getId()); + + return $this->json([ + 'entry' => $entry->toArray(), + 'totalDuration' => $this->formatMinutes($totalMin), + ], 201); + } + + // ── API: Eintrag bearbeiten ─────────────────────────────────────────────── + + #[Route('/api/entries/{id}', name: 'api_entries_update', methods: ['PATCH'])] + public function apiUpdate(int $id, Request $request): JsonResponse + { + $entry = $this->timeEntryRepo->find($id); + if (!$entry) { + return $this->json(['error' => 'Nicht gefunden'], 404); + } + + $data = json_decode($request->getContent(), true); + $project = $this->projectRepo->find($data['projectId'] ?? 0); + + if (!$project) { + return $this->json(['error' => 'Projekt nicht gefunden'], 400); + } + + $service = null; + if (!empty($data['serviceId'])) { + $service = $this->serviceRepo->find($data['serviceId']); + } + + $entry->setProject($project); + $entry->setService($service); + $entry->setDuration($this->parseDuration($data['duration'] ?? '0')); + $entry->setNote(!empty($data['note']) ? $data['note'] : null); + + $this->tenantEm->flush(); + + $totalMin = $this->timeEntryRepo->sumDurationByDateAndUserId( + $entry->getDate(), + $entry->getUserId(), + ); + + return $this->json([ + 'entry' => $entry->toArray(), + 'totalDuration' => $this->formatMinutes($totalMin), + ]); + } + + // ── API: Eintrag löschen ────────────────────────────────────────────────── + + #[Route('/api/entries/{id}', name: 'api_entries_delete', methods: ['DELETE'])] + public function apiDelete(int $id): JsonResponse + { + $entry = $this->timeEntryRepo->find($id); + if (!$entry) { + return $this->json(['error' => 'Nicht gefunden'], 404); + } + + $date = $entry->getDate(); + $userId = $entry->getUserId(); + + $this->tenantEm->remove($entry); + $this->tenantEm->flush(); + + $totalMin = $this->timeEntryRepo->sumDurationByDateAndUserId($date, $userId); + + return $this->json(['totalDuration' => $this->formatMinutes($totalMin)]); + } + + // ── Legacy-Routen ───────────────────────────────────────────────────────── + + #[Route('/day/{date}', name: 'timetracking_day')] + public function day(string $date): Response + { + return $this->redirectToRoute('timetracking_week_date', ['date' => $date]); + } + + #[Route('/copy-last/{date}', name: 'timetracking_copy_last')] + public function copyLast(): Response + { + return $this->redirectToRoute('timetracking_week'); + } + + // ── Hilfsfunktionen ─────────────────────────────────────────────────────── + + private function buildWeekDays(\DateTimeImmutable $activeDate, \DateTimeImmutable $today): array + { + $monday = $activeDate->modify('monday this week'); + $dayNames = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + $days = []; + + for ($i = 0; $i < 7; $i++) { + $day = $monday->modify("+{$i} days"); + $days[] = [ + 'date' => $day, + 'label' => $dayNames[$i], + 'short' => $dayNames[$i], + 'isToday' => $day->format('Y-m-d') === $today->format('Y-m-d'), + 'isActive' => $day->format('Y-m-d') === $activeDate->format('Y-m-d'), + ]; + } + + return $days; + } + + private function getGreeting(): string + { + $hour = (int) (new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')))->format('H'); + + return match(true) { + $hour >= 5 && $hour < 11 => 'Guten Morgen', + $hour >= 11 && $hour < 14 => 'Mahlzeit', + $hour >= 14 && $hour < 18 => 'Guten Tag', + $hour >= 18 && $hour < 22 => 'Guten Abend', + default => 'Gute Nacht', + }; + } + + private function parseDuration(string $input): int + { + $input = trim($input); + + if (str_contains($input, ':')) { + [$h, $m] = explode(':', $input, 2); + return (int) $h * 60 + (int) $m; + } + + if (str_contains($input, '.') || str_contains($input, ',')) { + return (int) round((float) str_replace(',', '.', $input) * 60); + } + + return (int) $input * 60; + } + + private function formatMinutes(int $minutes): string + { + return sprintf('%d:%02d', intdiv($minutes, 60), $minutes % 60); + } +} \ No newline at end of file diff --git a/httpdocs/src/Doctrine/TenantConnectionMiddleware.php b/httpdocs/src/Doctrine/TenantConnectionMiddleware.php new file mode 100644 index 0000000..3d14fc3 --- /dev/null +++ b/httpdocs/src/Doctrine/TenantConnectionMiddleware.php @@ -0,0 +1,38 @@ +tenantContext) extends AbstractDriverMiddleware { + public function __construct( + Driver $driver, + private readonly TenantContext $tenantContext, + ) { + parent::__construct($driver); + } + + public function connect(array $params): DriverConnection + { + if ($this->tenantContext->hasAccount()) { + $params['dbname'] = $this->tenantContext->getAccount()->getTenantDbName(); + } + return parent::connect($params); + } + }; + } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/.gitignore b/httpdocs/src/Entity/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/httpdocs/src/Entity/Central/Account.php b/httpdocs/src/Entity/Central/Account.php new file mode 100644 index 0000000..805582e --- /dev/null +++ b/httpdocs/src/Entity/Central/Account.php @@ -0,0 +1,70 @@ + 1])] + private int $trackingInterval = 1; + + #[ORM\Column] + private \DateTimeImmutable $createdAt; + + #[ORM\OneToMany(targetEntity: AccountUser::class, mappedBy: 'account', cascade: ['persist', 'remove'])] + private Collection $accountUsers; + + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + $this->accountUsers = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } + + public function getName(): string { return $this->name; } + public function setName(string $name): static { $this->name = $name; return $this; } + + public function getSlug(): string { return $this->slug; } + public function setSlug(string $slug): static { $this->slug = $slug; return $this; } + + public function getTrackingInterval(): int { return $this->trackingInterval; } + public function setTrackingInterval(int $v): static { $this->trackingInterval = $v; return $this; } + + public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } + + public function getAccountUsers(): Collection { return $this->accountUsers; } + + /** Gibt alle User zurück, die Admin dieses Accounts sind */ + public function getAdmins(): Collection + { + return $this->accountUsers->filter( + fn(AccountUser $au) => $au->getRole() === AccountUser::ROLE_ADMIN + ); + } + + public function getTenantDbName(): string + { + return 'db_' . str_replace('-', '_', $this->slug); + } + + public function __toString(): string { return $this->name; } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Central/AccountUser.php b/httpdocs/src/Entity/Central/AccountUser.php new file mode 100644 index 0000000..54eb647 --- /dev/null +++ b/httpdocs/src/Entity/Central/AccountUser.php @@ -0,0 +1,65 @@ +id; } + + public function getAccount(): ?Account { return $this->account; } + public function setAccount(?Account $account): static { $this->account = $account; return $this; } + + public function getUser(): ?User { return $this->user; } + public function setUser(?User $user): static { $this->user = $user; return $this; } + + public function getRole(): string { return $this->role; } + public function setRole(string $role): static { $this->role = $role; return $this; } + + public function getArchivedAt(): ?\DateTimeImmutable { return $this->archivedAt; } + public function setArchivedAt(?\DateTimeImmutable $archivedAt): static { $this->archivedAt = $archivedAt; return $this; } + public function isArchived(): bool { return $this->archivedAt !== null; } + + public function isAdmin(): bool { return $this->role === self::ROLE_ADMIN; } + public function isMember(): bool { return $this->role === self::ROLE_MEMBER; } + public function isTracker(): bool { return $this->role === self::ROLE_TRACKER; } + public function isMemberOrAdmin(): bool { return $this->isAdmin() || $this->isMember(); } + + public function getRoleLabel(): string + { + return match ($this->role) { + self::ROLE_ADMIN => 'Administrator', + self::ROLE_MEMBER => 'Standard', + self::ROLE_TRACKER => 'Zeiterfasser', + default => $this->role, + }; + } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Central/InviteToken.php b/httpdocs/src/Entity/Central/InviteToken.php new file mode 100644 index 0000000..182575a --- /dev/null +++ b/httpdocs/src/Entity/Central/InviteToken.php @@ -0,0 +1,66 @@ +createdAt = new \DateTimeImmutable(); + $this->expiresAt = new \DateTimeImmutable('+7 days'); + } + + public function isExpired(): bool { return $this->expiresAt < new \DateTimeImmutable(); } + + public function getId(): ?int { return $this->id; } + public function getToken(): string { return $this->token; } + public function setToken(string $token): static { $this->token = $token; return $this; } + public function getAccount(): ?Account { return $this->account; } + public function setAccount(?Account $account): static { $this->account = $account; return $this; } + public function getEmail(): string { return $this->email; } + public function setEmail(string $email): static { $this->email = $email; return $this; } + public function getFirstName(): string { return $this->firstName; } + public function setFirstName(string $firstName): static { $this->firstName = $firstName; return $this; } + public function getLastName(): string { return $this->lastName; } + public function setLastName(string $lastName): static { $this->lastName = $lastName; return $this; } + public function getRole(): string { return $this->role; } + public function setRole(string $role): static { $this->role = $role; return $this; } + public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } + public function getExpiresAt(): \DateTimeImmutable { return $this->expiresAt; } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Central/RegistrationToken.php b/httpdocs/src/Entity/Central/RegistrationToken.php new file mode 100644 index 0000000..d6079cf --- /dev/null +++ b/httpdocs/src/Entity/Central/RegistrationToken.php @@ -0,0 +1,69 @@ +createdAt = new \DateTimeImmutable(); + $this->expiresAt = new \DateTimeImmutable('+24 hours'); + } + + public function isExpired(): bool { return $this->expiresAt < new \DateTimeImmutable(); } + + public function getId(): ?int { return $this->id; } + public function getToken(): string { return $this->token; } + public function setToken(string $token): static { $this->token = $token; return $this; } + public function getCompanyName(): string { return $this->companyName; } + public function setCompanyName(string $companyName): static { $this->companyName = $companyName; return $this; } + public function getSlug(): string { return $this->slug; } + public function setSlug(string $slug): static { $this->slug = $slug; return $this; } + public function getEmail(): string { return $this->email; } + public function setEmail(string $email): static { $this->email = $email; return $this; } + public function getFirstName(): string { return $this->firstName; } + public function setFirstName(string $firstName): static { $this->firstName = $firstName; return $this; } + public function getLastName(): string { return $this->lastName; } + public function setLastName(string $lastName): static { $this->lastName = $lastName; return $this; } + public function getPasswordHash(): string { return $this->passwordHash; } + public function setPasswordHash(string $passwordHash): static { $this->passwordHash = $passwordHash; return $this; } + public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } + public function getExpiresAt(): \DateTimeImmutable { return $this->expiresAt; } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Central/User.php b/httpdocs/src/Entity/Central/User.php new file mode 100644 index 0000000..fbd0db1 --- /dev/null +++ b/httpdocs/src/Entity/Central/User.php @@ -0,0 +1,59 @@ +email; } + public function getRoles(): array { return ['ROLE_USER']; } + public function eraseCredentials(): void {} + + public function getId(): ?int { return $this->id; } + + public function getEmail(): string { return $this->email; } + public function setEmail(string $email): static { $this->email = $email; return $this; } + + public function getFirstName(): string { return $this->firstName; } + public function setFirstName(string $firstName): static { $this->firstName = $firstName; return $this; } + + public function getLastName(): string { return $this->lastName; } + public function setLastName(string $lastName): static { $this->lastName = $lastName; return $this; } + + public function getFullName(): string { return $this->firstName . ' ' . $this->lastName; } + + public function getPassword(): ?string { return $this->password; } + public function setPassword(?string $password): static { $this->password = $password; return $this; } + + public function getNote(): ?string { return $this->note; } + public function setNote(?string $note): static { $this->note = $note; return $this; } + + public function __toString(): string { return $this->getFullName(); } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Tenant/Client.php b/httpdocs/src/Entity/Tenant/Client.php new file mode 100644 index 0000000..93da328 --- /dev/null +++ b/httpdocs/src/Entity/Tenant/Client.php @@ -0,0 +1,71 @@ + 'ASC'])] + private Collection $projects; + + public function __construct() + { + $this->projects = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } + + public function getName(): string { return $this->name; } + public function setName(string $name): static { $this->name = $name; return $this; } + + public function getHourlyRate(): ?string { return $this->hourlyRate; } + public function setHourlyRate(?string $hourlyRate): static { $this->hourlyRate = $hourlyRate; return $this; } + + public function getNote(): ?string { return $this->note; } + public function setNote(?string $note): static { $this->note = $note; return $this; } + + public function getArchivedAt(): ?\DateTimeImmutable { return $this->archivedAt; } + public function setArchivedAt(?\DateTimeImmutable $archivedAt): static { $this->archivedAt = $archivedAt; return $this; } + public function isArchived(): bool { return $this->archivedAt !== null; } + + + public function getProjects(): Collection { return $this->projects; } + + public function addProject(Project $project): static + { + if (!$this->projects->contains($project)) { + $this->projects->add($project); + $project->setClient($this); + } + return $this; + } + + public function __toString(): string { return $this->name; } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Tenant/Project.php b/httpdocs/src/Entity/Tenant/Project.php new file mode 100644 index 0000000..d4210da --- /dev/null +++ b/httpdocs/src/Entity/Tenant/Project.php @@ -0,0 +1,47 @@ +id; } + + public function getName(): string { return $this->name; } + public function setName(string $name): static { $this->name = $name; return $this; } + + public function getClient(): ?Client { return $this->client; } + public function setClient(?Client $client): static { $this->client = $client; return $this; } + + public function getNote(): ?string { return $this->note; } + public function setNote(?string $note): static { $this->note = $note; return $this; } + + public function getArchivedAt(): ?\DateTimeImmutable { return $this->archivedAt; } + public function setArchivedAt(?\DateTimeImmutable $archivedAt): static { $this->archivedAt = $archivedAt; return $this; } + public function isArchived(): bool { return $this->archivedAt !== null; } + + public function __toString(): string { return $this->name; } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Tenant/Service.php b/httpdocs/src/Entity/Tenant/Service.php new file mode 100644 index 0000000..311bf85 --- /dev/null +++ b/httpdocs/src/Entity/Tenant/Service.php @@ -0,0 +1,46 @@ +id; } + + public function getName(): string { return $this->name; } + public function setName(string $name): static { $this->name = $name; return $this; } + + public function isBillable(): bool { return $this->billable; } + public function setBillable(bool $billable): static { $this->billable = $billable; return $this; } + + public function getNote(): ?string { return $this->note; } + public function setNote(?string $note): static { $this->note = $note; return $this; } + + public function getArchivedAt(): ?\DateTimeImmutable { return $this->archivedAt; } + public function setArchivedAt(?\DateTimeImmutable $archivedAt): static { $this->archivedAt = $archivedAt; return $this; } + public function isArchived(): bool { return $this->archivedAt !== null; } + + public function __toString(): string { return $this->name; } +} \ No newline at end of file diff --git a/httpdocs/src/Entity/Tenant/TimeEntry.php b/httpdocs/src/Entity/Tenant/TimeEntry.php new file mode 100644 index 0000000..980eb48 --- /dev/null +++ b/httpdocs/src/Entity/Tenant/TimeEntry.php @@ -0,0 +1,102 @@ +createdAt = new \DateTimeImmutable(); + $this->updatedAt = new \DateTimeImmutable(); + $this->date = new \DateTimeImmutable('today'); + } + + #[ORM\PreUpdate] + public function onPreUpdate(): void + { + $this->updatedAt = new \DateTimeImmutable(); + } + + public function getId(): ?int { return $this->id; } + + public function getDate(): \DateTimeImmutable { return $this->date; } + public function setDate(\DateTimeImmutable $date): static { $this->date = $date; return $this; } + + public function getDuration(): int { return $this->duration; } + public function setDuration(int $duration): static { $this->duration = $duration; return $this; } + + public function getDurationFormatted(): string + { + return sprintf('%d:%02d', intdiv($this->duration, 60), $this->duration % 60); + } + + public function getUserId(): int { return $this->userId; } + public function setUserId(int $userId): static { $this->userId = $userId; return $this; } + + public function getProject(): ?Project { return $this->project; } + public function setProject(?Project $project): static { $this->project = $project; return $this; } + + public function getService(): ?Service { return $this->service; } + public function setService(?Service $service): static { $this->service = $service; return $this; } + + public function getNote(): ?string { return $this->note; } + public function setNote(?string $note): static { $this->note = $note; return $this; } + + public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } + public function getUpdatedAt(): \DateTimeImmutable { return $this->updatedAt; } + + public function toArray(): array + { + return [ + 'id' => $this->id, + 'duration' => $this->duration, + 'durationFormatted' => $this->getDurationFormatted(), + 'projectId' => $this->project?->getId(), + 'projectName' => $this->project?->getName(), + 'clientName' => $this->project?->getClient()?->getName(), + 'serviceId' => $this->service?->getId(), + 'serviceName' => $this->service?->getName(), + 'serviceBillable' => $this->service?->isBillable(), + 'note' => $this->note, + ]; + } +} \ No newline at end of file diff --git a/httpdocs/src/EventSubscriber/SlidingSessionSubscriber.php b/httpdocs/src/EventSubscriber/SlidingSessionSubscriber.php new file mode 100644 index 0000000..c7a954b --- /dev/null +++ b/httpdocs/src/EventSubscriber/SlidingSessionSubscriber.php @@ -0,0 +1,57 @@ +isMainRequest()) { + return; + } + + $request = $event->getRequest(); + + if (!$request->hasSession()) { + return; + } + + $session = $request->getSession(); + + if (!$session->isStarted()) { + return; + } + + $params = session_get_cookie_params(); + + $event->getResponse()->headers->setCookie(new Cookie( + session_name(), + $session->getId(), + time() + self::LIFETIME, + $params['path'] ?: '/', + $params['domain'] ?: null, + $params['secure'], + $params['httponly'], + false, + $params['samesite'] ?: 'lax', + )); + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::RESPONSE => 'onKernelResponse', + ]; + } +} \ No newline at end of file diff --git a/httpdocs/src/EventSubscriber/TenantRequestSubscriber.php b/httpdocs/src/EventSubscriber/TenantRequestSubscriber.php new file mode 100644 index 0000000..e89ce14 --- /dev/null +++ b/httpdocs/src/EventSubscriber/TenantRequestSubscriber.php @@ -0,0 +1,56 @@ +isMainRequest()) { + return; + } + + $host = $event->getRequest()->getHost(); + + // Port aus appDomain entfernen, falls vorhanden (z.B. :8459 in DDEV) + $appDomainHost = explode(':', $this->appDomain)[0]; + + if (!str_ends_with($host, '.' . $appDomainHost)) { + return; + } + + $slug = str_replace('.' . $appDomainHost, '', $host); + $account = $this->accountRepo->findBySlug($slug); + + if ($account === null) { + // Unbekannter Slug → 404 oder Hauptdomain + $event->setResponse(new RedirectResponse('https://' . $this->appDomain)); + return; + } + + $this->tenantContext->setAccount($account); + } + + public static function getSubscribedEvents(): array + { + // Priorität 20: läuft vor Firewall (8), aber nach Router (32) + return [ + KernelEvents::REQUEST => ['onKernelRequest', 20], + ]; + } +} \ No newline at end of file diff --git a/httpdocs/src/Kernel.php b/httpdocs/src/Kernel.php new file mode 100644 index 0000000..779cd1f --- /dev/null +++ b/httpdocs/src/Kernel.php @@ -0,0 +1,11 @@ +findOneBy(['slug' => $slug]); + } +} \ No newline at end of file diff --git a/httpdocs/src/Repository/Central/AccountUserRepository.php b/httpdocs/src/Repository/Central/AccountUserRepository.php new file mode 100644 index 0000000..20fbcfa --- /dev/null +++ b/httpdocs/src/Repository/Central/AccountUserRepository.php @@ -0,0 +1,10 @@ +findOneBy(['token' => $token]); + } + + public function isSlugTaken(string $slug): bool + { + return $this->count(['slug' => $slug]) > 0; + } +} \ No newline at end of file diff --git a/httpdocs/src/Repository/Central/UserRepository.php b/httpdocs/src/Repository/Central/UserRepository.php new file mode 100644 index 0000000..07f6cc9 --- /dev/null +++ b/httpdocs/src/Repository/Central/UserRepository.php @@ -0,0 +1,15 @@ +findOneBy(['email' => $email]); + } +} \ No newline at end of file diff --git a/httpdocs/src/Repository/Tenant/ClientRepository.php b/httpdocs/src/Repository/Tenant/ClientRepository.php new file mode 100644 index 0000000..51f8116 --- /dev/null +++ b/httpdocs/src/Repository/Tenant/ClientRepository.php @@ -0,0 +1,32 @@ +createQueryBuilder('c') + ->orderBy('c.name', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findAllActiveOrderedByName(): array + { + return $this->createQueryBuilder('c') + ->where('c.archivedAt IS NULL') + ->orderBy('c.name', 'ASC') + ->getQuery() + ->getResult(); + } +} diff --git a/httpdocs/src/Repository/Tenant/ProjectRepository.php b/httpdocs/src/Repository/Tenant/ProjectRepository.php new file mode 100644 index 0000000..b0a166d --- /dev/null +++ b/httpdocs/src/Repository/Tenant/ProjectRepository.php @@ -0,0 +1,39 @@ +createQueryBuilder('p') + ->join('p.client', 'c') + ->addSelect('c') + ->orderBy('c.name', 'ASC') + ->addOrderBy('p.name', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findAllActiveWithClient(): array + { + return $this->createQueryBuilder('p') + ->join('p.client', 'c') + ->addSelect('c') + ->where('p.archivedAt IS NULL') + ->andWhere('c.archivedAt IS NULL') + ->orderBy('c.name', 'ASC') + ->addOrderBy('p.name', 'ASC') + ->getQuery() + ->getResult(); + } +} diff --git a/httpdocs/src/Repository/Tenant/ServiceRepository.php b/httpdocs/src/Repository/Tenant/ServiceRepository.php new file mode 100644 index 0000000..f43b6aa --- /dev/null +++ b/httpdocs/src/Repository/Tenant/ServiceRepository.php @@ -0,0 +1,34 @@ +createQueryBuilder('s') + ->orderBy('s.billable', 'DESC') + ->addOrderBy('s.name', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findAllActiveOrderedByBillable(): array + { + return $this->createQueryBuilder('s') + ->where('s.archivedAt IS NULL') + ->orderBy('s.billable', 'DESC') + ->addOrderBy('s.name', 'ASC') + ->getQuery() + ->getResult(); + } +} diff --git a/httpdocs/src/Repository/Tenant/TimeEntryRepository.php b/httpdocs/src/Repository/Tenant/TimeEntryRepository.php new file mode 100644 index 0000000..5f53573 --- /dev/null +++ b/httpdocs/src/Repository/Tenant/TimeEntryRepository.php @@ -0,0 +1,89 @@ +createQueryBuilder('t') + ->join('t.project', 'p') + ->join('p.client', 'c') + ->leftJoin('t.service', 's') + ->addSelect('p', 'c', 's') + ->where('t.date = :date') + ->andWhere('t.userId = :userId') + ->setParameter('date', $date->format('Y-m-d')) + ->setParameter('userId', $userId) + ->orderBy('t.createdAt', 'ASC') + ->getQuery() + ->getResult(); + } + + public function sumDurationByDateAndUserId(\DateTimeImmutable $date, int $userId): int + { + $result = $this->createQueryBuilder('t') + ->select('SUM(t.duration) as total') + ->where('t.date = :date') + ->andWhere('t.userId = :userId') + ->setParameter('date', $date->format('Y-m-d')) + ->setParameter('userId', $userId) + ->getQuery() + ->getSingleScalarResult(); + + return (int) $result; + } + + public function countByProject(Project $project): int + { + return (int) $this->createQueryBuilder('t') + ->select('COUNT(t.id)') + ->where('t.project = :project') + ->setParameter('project', $project) + ->getQuery() + ->getSingleScalarResult(); + } + + public function countByService(Service $service): int + { + return (int) $this->createQueryBuilder('t') + ->select('COUNT(t.id)') + ->where('t.service = :service') + ->setParameter('service', $service) + ->getQuery() + ->getSingleScalarResult(); + } + + public function countByClient(Client $client): int + { + return (int) $this->createQueryBuilder('t') + ->select('COUNT(t.id)') + ->join('t.project', 'p') + ->where('p.client = :client') + ->setParameter('client', $client) + ->getQuery() + ->getSingleScalarResult(); + } + + public function countByUserId(int $userId): int + { + return (int) $this->createQueryBuilder('t') + ->select('COUNT(t.id)') + ->where('t.userId = :userId') + ->setParameter('userId', $userId) + ->getQuery() + ->getSingleScalarResult(); + } +} \ No newline at end of file diff --git a/httpdocs/src/Security/AccessDeniedHandler.php b/httpdocs/src/Security/AccessDeniedHandler.php new file mode 100644 index 0000000..de2545f --- /dev/null +++ b/httpdocs/src/Security/AccessDeniedHandler.php @@ -0,0 +1,22 @@ +urlGenerator->generate('timetracking_week')); + } +} \ No newline at end of file diff --git a/httpdocs/src/Service/AccountRoleHelper.php b/httpdocs/src/Service/AccountRoleHelper.php new file mode 100644 index 0000000..ace2ed3 --- /dev/null +++ b/httpdocs/src/Service/AccountRoleHelper.php @@ -0,0 +1,40 @@ +security->getUser(); + $account = $this->tenantContext->getAccount(); + + if ($user === null || $account === null) { + return null; + } + + return $this->accountUserRepo->findOneBy([ + 'account' => $account, + 'user' => $user, + ]); + } + + public function isAdmin(): bool { return $this->getCurrentAccountUser()?->isAdmin() ?? false; } + public function isMember(): bool { return $this->getCurrentAccountUser()?->isMember() ?? false; } + public function isTracker(): bool { return $this->getCurrentAccountUser()?->isTracker() ?? false; } + public function isMemberOrAdmin(): bool { return $this->getCurrentAccountUser()?->isMemberOrAdmin() ?? false; } +} \ No newline at end of file diff --git a/httpdocs/src/Service/RegistrationService.php b/httpdocs/src/Service/RegistrationService.php new file mode 100644 index 0000000..0a887ed --- /dev/null +++ b/httpdocs/src/Service/RegistrationService.php @@ -0,0 +1,192 @@ +centralEm->getRepository(User::class)->findOneBy(['email' => $email]); + if ($existingUser !== null) { + throw new \DomainException('Diese E-Mail-Adresse wird bereits verwendet.'); + } + + // Pending Token für dieselbe E-Mail? (doppeltes Absenden verhindern) + $existingToken = $this->centralEm->getRepository(RegistrationToken::class)->findOneBy(['email' => $email]); + if ($existingToken !== null) { + // Einfach den alten überschreiben + $this->centralEm->remove($existingToken); + $this->centralEm->flush(); + } + + $slug = $this->slugGenerator->generateFromCompanyName($companyName); + + // Dummy-User für den Hasher (braucht UserInterface) + $tempUser = new User(); + $passwordHash = $this->passwordHasher->hashPassword($tempUser, $plainPassword); + + $token = new RegistrationToken(); + $token->setToken(bin2hex(random_bytes(32))); + $token->setCompanyName($companyName); + $token->setSlug($slug); + $token->setEmail($email); + $token->setFirstName($firstName); + $token->setLastName($lastName); + $token->setPasswordHash($passwordHash); + + $this->centralEm->persist($token); + $this->centralEm->flush(); + + $this->sendConfirmationMail($token); + + return $token; + } + + /** + * Schritt 2: Token bestätigen, Account + Tenant anlegen, Mails raus. + */ + public function confirm(string $tokenString): Account + { + $token = $this->centralEm->getRepository(RegistrationToken::class)->findOneBy(['token' => $tokenString]); + + if ($token === null) { + throw new \InvalidArgumentException('Ungültiger Bestätigungslink.'); + } + + if ($token->isExpired()) { + $this->centralEm->remove($token); + $this->centralEm->flush(); + throw new \InvalidArgumentException('Dieser Link ist abgelaufen (gültig 24 Stunden). Bitte registriere dich erneut.'); + } + + // Account anlegen + $account = new Account(); + $account->setName($token->getCompanyName()); + $account->setSlug($token->getSlug()); + $this->centralEm->persist($account); + + // User anlegen + $user = new User(); + $user->setEmail($token->getEmail()); + $user->setFirstName($token->getFirstName()); + $user->setLastName($token->getLastName()); + $user->setPassword($token->getPasswordHash()); + $this->centralEm->persist($user); + + // AccountUser (Admin) + $accountUser = new AccountUser(); + $accountUser->setAccount($account); + $accountUser->setUser($user); + $accountUser->setRole(AccountUser::ROLE_ADMIN); + $this->centralEm->persist($accountUser); + + $this->centralEm->flush(); + + // Tenant-Datenbank anlegen + $this->tenantContext->setAccount($account); + $this->tenantEm->getConnection()->close(); + + // Datenbank erstellen (User braucht CREATE-Recht auf db_%): + $dbName = $account->getTenantDbName(); + $this->centralEm->getConnection()->executeStatement( + 'CREATE DATABASE IF NOT EXISTS `' . $dbName . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' + ); + + $schemaTool = new SchemaTool($this->tenantEm); + $schemaTool->createSchema($this->tenantEm->getMetadataFactory()->getAllMetadata()); + + // Token löschen + $this->centralEm->remove($token); + $this->centralEm->flush(); + + // Mails + $this->sendWelcomeMail($user, $account); + $this->sendNotifyMail($user, $account); + + return $account; + } + + private function sendConfirmationMail(RegistrationToken $token): void + { + $verifyUrl = $this->urlGenerator->generate( + 'app_verify', + ['token' => $token->getToken()], + UrlGeneratorInterface::ABSOLUTE_URL, + ); + + $email = (new TemplatedEmail()) + ->to(new Address($token->getEmail(), $token->getFirstName() . ' ' . $token->getLastName())) + ->subject('Bitte bestätige deine Registrierung – spawntree Timetracker') + ->htmlTemplate('email/registration_confirm.html.twig') + ->context([ + 'token' => $token, + 'verifyUrl' => $verifyUrl, + ]); + + $this->mailer->send($email); + } + + private function sendWelcomeMail(User $user, Account $account): void + { + $loginUrl = 'https://' . $account->getSlug() . '.' . $this->appDomain; + + $email = (new TemplatedEmail()) + ->to(new Address($user->getEmail(), $user->getFullName())) + ->subject('Willkommen beim spawntree Timetracker!') + ->htmlTemplate('email/registration_welcome.html.twig') + ->context([ + 'user' => $user, + 'account' => $account, + 'loginUrl' => $loginUrl, + ]); + + $this->mailer->send($email); + } + + private function sendNotifyMail(User $user, Account $account): void + { + $email = (new TemplatedEmail()) + ->to($this->notifyEmail) + ->subject('[Timetracker] Neue Registrierung: ' . $account->getName()) + ->htmlTemplate('email/registration_notify.html.twig') + ->context([ + 'user' => $user, + 'account' => $account, + ]); + + $this->mailer->send($email); + } +} \ No newline at end of file diff --git a/httpdocs/src/Service/SlugGenerator.php b/httpdocs/src/Service/SlugGenerator.php new file mode 100644 index 0000000..b2e224d --- /dev/null +++ b/httpdocs/src/Service/SlugGenerator.php @@ -0,0 +1,165 @@ +truncate($slug, 16); + + // 7. Eindeutigkeit sicherstellen + return $this->makeUnique($slug); + } + + /** + * Nur für die Live-Vorschau im Formular – ohne Uniqueness-Check. + */ + public function previewFromCompanyName(string $companyName): string + { + $slug = $companyName; + $slug = preg_replace(self::LEGAL_FORMS, '', $slug); + $slug = str_replace( + ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß'], + ['ae', 'oe', 'ue', 'ae', 'oe', 'ue', 'ss'], + $slug + ); + $slug = mb_strtolower($slug); + $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); + $slug = trim($slug, '-'); + + if ($slug === '') { + throw new \InvalidArgumentException('Kein gültiger Slug.'); + } + + return $this->truncate($slug, 16); + } + + private function truncate(string $slug, int $max): string + { + if (strlen($slug) <= $max) { + return $slug; + } + + $cut = substr($slug, 0, $max); + $lastDash = strrpos($cut, '-'); + + // Nur an Bindestrich schneiden wenn der Rest noch sinnvoll lang ist + if ($lastDash !== false && $lastDash >= 4) { + return substr($cut, 0, $lastDash); + } + + return rtrim($cut, '-'); + } + + private function makeUnique(string $base): string + { + if (!$this->isTaken($base)) { + return $base; + } + + // Basis auf 14 Zeichen kürzen damit -NN noch reinpasst + $base = $this->truncate($base, 14); + + for ($i = 2; $i <= 99; $i++) { + $candidate = $base . '-' . $i; + if (!$this->isTaken($candidate)) { + return $candidate; + } + } + + throw new \RuntimeException('Konnte keinen eindeutigen Slug generieren.'); + } + + private function isTaken(string $slug): bool + { + return $this->accountRepo->findBySlug($slug) !== null + || $this->tokenRepo->isSlugTaken($slug); + } +} \ No newline at end of file diff --git a/httpdocs/src/Service/TenantContext.php b/httpdocs/src/Service/TenantContext.php new file mode 100644 index 0000000..8f59c89 --- /dev/null +++ b/httpdocs/src/Service/TenantContext.php @@ -0,0 +1,29 @@ +account = $account; + } + + public function getAccount(): ?Account + { + return $this->account; + } + + public function hasAccount(): bool + { + return $this->account !== null; + } +} \ No newline at end of file diff --git a/httpdocs/src/Twig/AppExtension.php b/httpdocs/src/Twig/AppExtension.php new file mode 100644 index 0000000..e6b40e4 --- /dev/null +++ b/httpdocs/src/Twig/AppExtension.php @@ -0,0 +1,77 @@ +roleHelper->isAdmin(); } + public function isCurrentUserMemberOrAdmin(): bool { return $this->roleHelper->isMemberOrAdmin(); } + public function getCurrentUserRole(): string { return $this->roleHelper->getCurrentAccountUser()?->getRole() ?? ''; } + + public function formatDuration(int $minutes): string + { + $h = intdiv($minutes, 60); + $m = $minutes % 60; + return sprintf('%d:%02d', $h, $m); + } + + public function deMonths(): array + { + return [ + 'Januar','Februar','März','April','Mai','Juni', + 'Juli','August','September','Oktober','November','Dezember', + ]; + } + + public function deMonthsShort(): array + { + return [ + 'Jan','Feb','Mär','Apr','Mai','Jun', + 'Jul','Aug','Sep','Okt','Nov','Dez', + ]; + } + + public function deWeekdays(): array + { + return [ + 'Montag','Dienstag','Mittwoch','Donnerstag', + 'Freitag','Samstag','Sonntag', + ]; + } + + public function deWeekdaysShort(): array + { + return ['Mo','Di','Mi','Do','Fr','Sa','So']; + } +} \ No newline at end of file diff --git a/httpdocs/src/Twig/Runtime/AppExtensionRuntime.php b/httpdocs/src/Twig/Runtime/AppExtensionRuntime.php new file mode 100644 index 0000000..eb9e189 --- /dev/null +++ b/httpdocs/src/Twig/Runtime/AppExtensionRuntime.php @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/httpdocs/templates/account/index.html.twig b/httpdocs/templates/account/index.html.twig new file mode 100644 index 0000000..af614db --- /dev/null +++ b/httpdocs/templates/account/index.html.twig @@ -0,0 +1,219 @@ +{# templates/account/index.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %} + {% if tab == 'account' %}Account{% else %}Mein Benutzer{% endif %} +{% endblock %} + +{% block body %} + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/httpdocs/templates/base.html.twig b/httpdocs/templates/base.html.twig new file mode 100644 index 0000000..dd7e204 --- /dev/null +++ b/httpdocs/templates/base.html.twig @@ -0,0 +1,26 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {% block stylesheets %} + {{ encore_entry_link_tags('app') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app') }} + {% endblock %} + + {% set frankenphpHotReload = app.request.server.get('FRANKENPHP_HOT_RELOAD') %} + {% if frankenphpHotReload %} + + + + {% endif %} + + + {% include '_nav.html.twig' %} + {% block body %}{% endblock %} + + diff --git a/httpdocs/templates/client/index.html.twig b/httpdocs/templates/client/index.html.twig new file mode 100644 index 0000000..89736f8 --- /dev/null +++ b/httpdocs/templates/client/index.html.twig @@ -0,0 +1,133 @@ +{# templates/client/index.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}Kunden{% endblock %} + +{% block body %} + + + +
+ +
+

Kunden

+ +
+ +
+
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+ {% for client in clients %} +
+ +
+
+ {{ client.name }} + + {% set count = client.projects|length %} + {{ count }} {{ count == 1 ? 'Projekt' : 'Projekte' }} + +
+
+ {% if client.isArchived() %} + + {% else %} + + + {% endif %} +
+
+ + {% if not client.isArchived() %} + + {% endif %} + +
+ {% else %} +
+

Noch keine Kunden angelegt.

+
+ {% endfor %} +
+ +
+ +{% endblock %} + +{% block javascripts %} + {{ parent() }} + {{ encore_entry_script_tags('crud') }} +{% endblock %} diff --git a/httpdocs/templates/email/registration_confirm.html.twig b/httpdocs/templates/email/registration_confirm.html.twig new file mode 100644 index 0000000..781fd05 --- /dev/null +++ b/httpdocs/templates/email/registration_confirm.html.twig @@ -0,0 +1,35 @@ +{# templates/email/registration_confirm.html.twig #} + + + + +
+ +

spawntree Timetracker

+
+ +

Hallo {{ token.firstName }},

+

+ bitte bestätige deine Registrierung für {{ token.companyName }} + mit einem Klick auf den Button. +

+ + + +

+ Der Link ist 24 Stunden gültig (bis {{ token.expiresAt|date('d.m.Y H:i') }} Uhr).
+ Falls du dich nicht registriert hast, kannst du diese E-Mail ignorieren. +

+

+ {{ verifyUrl }} +

+ +
+ + \ No newline at end of file diff --git a/httpdocs/templates/email/registration_notify.html.twig b/httpdocs/templates/email/registration_notify.html.twig new file mode 100644 index 0000000..bca96f3 --- /dev/null +++ b/httpdocs/templates/email/registration_notify.html.twig @@ -0,0 +1,15 @@ +{# templates/email/registration_notify.html.twig #} + + + + +

Neue Registrierung im Timetracker

+ + + + + + +
Firma{{ account.name }}
Slug{{ account.slug }}
Name{{ user.fullName }}
E-Mail{{ user.email }}
Datum{{ account.createdAt|date('d.m.Y H:i') }}
+ + \ No newline at end of file diff --git a/httpdocs/templates/email/registration_welcome.html.twig b/httpdocs/templates/email/registration_welcome.html.twig new file mode 100644 index 0000000..a68b39a --- /dev/null +++ b/httpdocs/templates/email/registration_welcome.html.twig @@ -0,0 +1,30 @@ +{# templates/email/registration_welcome.html.twig #} + + + + +
+ +

spawntree Timetracker

+
+ +

Hallo {{ user.firstName }},

+

+ dein Konto für {{ account.name }} ist jetzt aktiv. Los geht's! +

+ + + +

+ Deine URL: {{ loginUrl }} +

+ +
+ + \ No newline at end of file diff --git a/httpdocs/templates/email/team_invite.html.twig b/httpdocs/templates/email/team_invite.html.twig new file mode 100644 index 0000000..bff716f --- /dev/null +++ b/httpdocs/templates/email/team_invite.html.twig @@ -0,0 +1,32 @@ +{# templates/email/team_invite.html.twig #} + + + + +
+ +

+ Du wurdest zu {{ invite.account.name }} eingeladen +

+

+ Hallo {{ invite.firstName }},
+ du wurdest als {{ invite.role == 'admin' ? 'Administrator' : (invite.role == 'tracker' ? 'Zeiterfasser' : 'Standard-Nutzer') }} + zum Team von {{ invite.account.name }} hinzugefügt. +

+ +

+ Klicke auf den Button, um dein Passwort festzulegen und loszulegen. Der Link ist 7 Tage gültig. +

+ + + Passwort festlegen → + + +

+ Wenn du diese Einladung nicht erwartet hast, kannst du diese E-Mail ignorieren. +

+
+ + \ No newline at end of file diff --git a/httpdocs/templates/home/index.html.twig b/httpdocs/templates/home/index.html.twig new file mode 100644 index 0000000..d8648aa --- /dev/null +++ b/httpdocs/templates/home/index.html.twig @@ -0,0 +1,28 @@ +{# templates/home/index.html.twig #} + + + + + + spawntree Timetracker + {{ encore_entry_link_tags('app') }} + + + +
+
+
spawntree Timetracker
+ Kostenlos starten +
+
+ +
+
+

Zeiterfassung,
die nicht nervt.

+

Einfach, schnell, ohne Overhead. Dein Team, deine Projekte, deine Zeit.

+ Jetzt registrieren → +
+
+ + + \ No newline at end of file diff --git a/httpdocs/templates/invite/error.html.twig b/httpdocs/templates/invite/error.html.twig new file mode 100644 index 0000000..d387d9f --- /dev/null +++ b/httpdocs/templates/invite/error.html.twig @@ -0,0 +1,16 @@ +{# templates/invite/error.html.twig #} + + + + + + Fehler – Einladungslink + {{ encore_entry_link_tags('app') }} + + + + + \ No newline at end of file diff --git a/httpdocs/templates/invite/set_password.html.twig b/httpdocs/templates/invite/set_password.html.twig new file mode 100644 index 0000000..baf256b --- /dev/null +++ b/httpdocs/templates/invite/set_password.html.twig @@ -0,0 +1,58 @@ +{# templates/invite/set_password.html.twig #} + + + + + + Passwort festlegen – {{ invite.account.name }} + {{ encore_entry_link_tags('app') }} + + + + + + + \ No newline at end of file diff --git a/httpdocs/templates/project/index.html.twig b/httpdocs/templates/project/index.html.twig new file mode 100644 index 0000000..35ab1d1 --- /dev/null +++ b/httpdocs/templates/project/index.html.twig @@ -0,0 +1,139 @@ +{# templates/project/index.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}Projekte{% endblock %} + +{% block body %} + + +
+ +
+

Projekte

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+ + {% for project in projects %} +
+ +
+
+ {{ project.name }} + {{ project.client.name }} +
+
+ {% if project.isArchived() %} + + {% else %} + + + {% endif %} +
+
+ + {% if not project.isArchived() %} + + {% endif %} + +
+ {% else %} +
+

Noch keine Projekte angelegt.

+
+ {% endfor %} + +
+
+ +{% endblock %} + +{% block javascripts %} + {{ parent() }} + {{ encore_entry_script_tags('crud') }} +{% endblock %} diff --git a/httpdocs/templates/registration/confirm_error.html.twig b/httpdocs/templates/registration/confirm_error.html.twig new file mode 100644 index 0000000..78c7118 --- /dev/null +++ b/httpdocs/templates/registration/confirm_error.html.twig @@ -0,0 +1,26 @@ +{# templates/registration/confirm_error.html.twig #} + + + + + + Fehler – spawntree Timetracker + {{ encore_entry_link_tags('app') }} + + + +
+
+
+
+

Link ungültig

+

{{ error }}

+ + Erneut registrieren + +
+
+
+ + + \ No newline at end of file diff --git a/httpdocs/templates/registration/confirmed.html.twig b/httpdocs/templates/registration/confirmed.html.twig new file mode 100644 index 0000000..e556113 --- /dev/null +++ b/httpdocs/templates/registration/confirmed.html.twig @@ -0,0 +1,30 @@ +{# templates/registration/confirmed.html.twig #} + + + + + + Konto aktiviert – spawntree Timetracker + {{ encore_entry_link_tags('app') }} + + + + +
+
+
+
+

Konto aktiviert!

+

+ Willkommen, {{ account.name }}.
+ Du wirst gleich weitergeleitet … +

+ + Jetzt anmelden → + +
+
+
+ + + \ No newline at end of file diff --git a/httpdocs/templates/registration/register.html.twig b/httpdocs/templates/registration/register.html.twig new file mode 100644 index 0000000..218a7ab --- /dev/null +++ b/httpdocs/templates/registration/register.html.twig @@ -0,0 +1,96 @@ +{# templates/registration/register.html.twig #} + + + + + + Registrieren – spawntree Timetracker + {{ encore_entry_link_tags('app') }} + + + +
+
+ + +

Konto erstellen

+

Kostenlos starten, keine Kreditkarte nötig.

+ + + +
+ +
+ Dein Unternehmen + +
+ + +
+ Deine voraussichtliche URL: +
+
+
+ +
+ Dein Konto + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+ +
+
+
+ + +{{ encore_entry_script_tags('registration') }} + + + \ No newline at end of file diff --git a/httpdocs/templates/security/login.html.twig b/httpdocs/templates/security/login.html.twig new file mode 100644 index 0000000..2a5b937 --- /dev/null +++ b/httpdocs/templates/security/login.html.twig @@ -0,0 +1,65 @@ +{# templates/security/login.html.twig #} + + + + + + Anmelden – spawntree + {{ encore_entry_link_tags('app') }} + + + + + + + \ No newline at end of file diff --git a/httpdocs/templates/service/index.html.twig b/httpdocs/templates/service/index.html.twig new file mode 100644 index 0000000..88e398c --- /dev/null +++ b/httpdocs/templates/service/index.html.twig @@ -0,0 +1,141 @@ +{# templates/service/index.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}Leistungen{% endblock %} + +{% block body %} + + +
+ +
+

Leistungen

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+ + {% set currentGroup = null %} + {% for service in services %} + + {% set group = service.billable ? 'Verrechenbar' : 'Nicht-verrechenbar' %} + {% if group != currentGroup %} + {% if currentGroup is not null %}
{% endif %} +
+
{{ group }}
+ {% set currentGroup = group %} + {% endif %} + +
+ +
+
+ {{ service.name }} +
+
+ {% if service.isArchived() %} + + {% else %} + + + {% endif %} +
+
+ + {% if not service.isArchived() %} + + {% endif %} + +
+ + {% else %} +
+

Noch keine Leistungen angelegt.

+
+ {% endfor %} + {% if currentGroup is not null %}
{% endif %} + +
+ + +{% endblock %} + +{% block javascripts %} + {{ parent() }} + {{ encore_entry_script_tags('crud') }} +{% endblock %} diff --git a/httpdocs/templates/team/index.html.twig b/httpdocs/templates/team/index.html.twig new file mode 100644 index 0000000..3ebdb45 --- /dev/null +++ b/httpdocs/templates/team/index.html.twig @@ -0,0 +1,237 @@ +{# templates/team/index.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}Team{% endblock %} + +{% block body %} +
+ +
+

Team

+ +
+ +
+ + +
+ + {# ── Aktive User ──────────────────────────────────────────────────── #} +
+ + {% for au in activeUsers %} +
+ +
+
+ {{ au.user.fullName }} + ({{ au.roleLabel }}) + {% if au.user.password is null %} + Einladung ausstehend + {% endif %} +
+
+ + {% if au.user.id != currentUserId %} + + {% endif %} +
+
+ + + +
+ {% endfor %} + + {# ── Ausstehende Einladungen ──────────────────────────────────── #} + {% for invite in pendingInvites %} +
+
+
+ {{ invite.firstName }} {{ invite.lastName }} + ({{ invite.email }}) + Einladung ausstehend +
+
+ +
+
+
+ {% endfor %} + + {% if activeUsers is empty and pendingInvites is empty %} +
Noch keine aktiven Teammitglieder.
+ {% endif %} + +
+ + {# ── Archivierte User ─────────────────────────────────────────────── #} + + +
+ + {# ── Einlade-Modal ────────────────────────────────────────────────────────────── #} + + +{% endblock %} + +{% block javascripts %} + {{ parent() }} + {{ encore_entry_script_tags('team') }} +{% endblock %} \ No newline at end of file diff --git a/httpdocs/templates/timetracking/_entry_row.html.twig b/httpdocs/templates/timetracking/_entry_row.html.twig new file mode 100644 index 0000000..6492db3 --- /dev/null +++ b/httpdocs/templates/timetracking/_entry_row.html.twig @@ -0,0 +1,81 @@ +{# templates/timetracking/_entry_row.html.twig #} +
+ +
+
+
+ {{ entry.project.client.name }} / {{ entry.project.name }} + {% if entry.service %} / {{ entry.service.name }}{% endif %} +
+ {% if entry.note %} +
{{ entry.note }}
+ {% endif %} +
+
+ {{ entry.durationFormatted }} + + +
+
+ + + +
diff --git a/httpdocs/templates/timetracking/week.html.twig b/httpdocs/templates/timetracking/week.html.twig new file mode 100644 index 0000000..81d360d --- /dev/null +++ b/httpdocs/templates/timetracking/week.html.twig @@ -0,0 +1,237 @@ +{# templates/timetracking/week.html.twig #} +{% extends 'base.html.twig' %} + +{# + Datums-Arrays kommen aus AppExtension (single source of truth). + Skalare Strings kommen aus messages.de.yaml via |trans. +#} +{% set months = deMonths() %} +{% set monthsShort = deMonthsShort() %} +{% set weekdays = deWeekdays() %} +{% set weekdaysShort= deWeekdaysShort() %} + +{% block title %} + {% set monthName = months[currentDate|date('n') - 1] %} + {% set activStr = currentDate|date('Y-m-d') %} + {% if activStr == todayStr %} + {{ 'app.date.today'|trans }}, {{ currentDate|date('j') }}. {{ monthName }} + {% elseif activStr == tomorrowStr %} + {{ 'app.date.tomorrow'|trans }}, {{ currentDate|date('j') }}. {{ monthName }} + {% elseif activStr == yesterdayStr %} + {{ 'app.date.yesterday'|trans }}, {{ currentDate|date('j') }}. {{ monthName }} + {% else %} + {{ weekdays[currentDate|date('N') - 1] }}, {{ currentDate|date('j') }}. {{ monthName }} + {% endif %} +{% endblock %} + +{% block body %} + + + +
+ +
+
+
+ {% set activStr = currentDate|date('Y-m-d') %} + {% set monthName = months[currentDate|date('n') - 1] %} + {% set weekdayIdx = currentDate|date('N') - 1 %} + {% if activStr == todayStr %} + {{ 'app.date.today'|trans }}, {{ currentDate|date('j') }}. {{ monthName }} + {% elseif activStr == tomorrowStr %} + {{ 'app.date.tomorrow'|trans }}, {{ currentDate|date('j') }}. {{ monthName }} + {% elseif activStr == yesterdayStr %} + {{ 'app.date.yesterday'|trans }}, {{ currentDate|date('j') }}. {{ monthName }} + {% else %} + {{ weekdays[weekdayIdx] }}, {{ currentDate|date('j') }}. {{ monthName }} + {% endif %} +
+
{{ 'app.date.week_label'|trans }} {{ currentWeekNumber }}
+
+ + +
+ +
+ {{ greeting }}, {{ firstName }}! +
+ +
+ +
+
+ + +
+ +
+ ? + {{ 'app.entry.duration_hint'|trans }} +
+
+ + +
+ + +
+ + +
+ +
+ +
+ +
+ +
+
+ +
+ {% if timeEntries is empty %} +
+

{{ 'app.entry.no_entries'|trans }}

+
+ {% else %} +
+ {% for entry in timeEntries %} + {% include 'timetracking/_entry_row.html.twig' with { entry: entry } %} + {% endfor %} +
+
+ {{ totalDuration }} +
+ {% endif %} +
+ +
+
+ +{% endblock %} + +{% block javascripts %} + {{ parent() }} +{% endblock %} diff --git a/httpdocs/translations/.gitignore b/httpdocs/translations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/httpdocs/translations/messages.de.yaml b/httpdocs/translations/messages.de.yaml new file mode 100644 index 0000000..d948f81 --- /dev/null +++ b/httpdocs/translations/messages.de.yaml @@ -0,0 +1,43 @@ +# translations/messages.de.yaml + +app: + date: + today: "Heute" + tomorrow: "Morgen" + yesterday: "Gestern" + week_label: "Kalenderwoche" + + nav: + prev_week: "Vorherige Woche" + next_week: "Nächste Woche" + month_view: "Monatsansicht öffnen/schließen" + prev_month: "Vorheriger Monat" + next_month: "Nächster Monat" + close_month: "Monatsansicht schließen" + + entry: + label_duration: "Dauer" + label_project_service: "Projekt / Leistung" + label_note: "Bemerkung" + placeholder_note: "Optionale Beschreibung …" + placeholder_duration_hint: "Format: 1:30 oder 1.5" + btn_create: "Erstellen" + btn_save: "Sichern" + btn_cancel: "Abbrechen" + btn_edit: "Bearbeiten" + btn_delete: "Löschen" + no_entries: "Noch keine Zeiteinträge für diesen Tag" + select_placeholder: "..." + confirm_delete: "Eintrag wirklich löschen?" + error_no_project: "Bitte ein Projekt wählen." + error_save: "Fehler beim Speichern des Eintrags." + error_delete: "Fehler beim Löschen." + error_load: "Fehler beim Laden der Einträge." + duration_hint: "1:30 für 1 Std 30 Min · 8 12 für 8 bis 12 Uhr · 1,75 für 1 Std 45 Min · 0:00 zum Stoppen" + error_zero_duration: "Bitte eine Dauer größer als 0:00 eingeben." + error_duration_too_long: "Eine Dauer von mehr als 24 Stunden ist nicht möglich." + warn_duration_long: "Die Dauer ist länger als 8 Stunden. Wirklich speichern?" + + service: + billable: "Verrechenbar" + not_billable: "Nicht-verrechenbar" diff --git a/httpdocs/webpack.config.js b/httpdocs/webpack.config.js new file mode 100644 index 0000000..7285130 --- /dev/null +++ b/httpdocs/webpack.config.js @@ -0,0 +1,79 @@ +const Encore = require('@symfony/webpack-encore'); + +// Manually configure the runtime environment if not already configured yet by the "encore" command. +// It's useful when you use tools that rely on webpack.config.js file. +if (!Encore.isRuntimeEnvironmentConfigured()) { + Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); +} + +Encore + // directory where compiled assets will be stored + .setOutputPath('public/build/') + // public path used by the web server to access the output path + .setPublicPath('/build') + // only needed for CDN's or subdirectory deploy + //.setManifestKeyPrefix('build/') + + /* + * ENTRY CONFIG + * + * Each entry will result in one JavaScript file (e.g. app.js) + * and one CSS file (e.g. app.css) if your JavaScript imports CSS. + */ + .addEntry('app', './assets/app.js') + .addEntry('crud', './assets/scripts/crud.js') + .addEntry('registration', './assets/scripts/registration.js') + .addEntry('team', './assets/scripts/team.js') + + // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. + .splitEntryChunks() + + // will require an extra script tag for runtime.js + // but, you probably want this, unless you're building a single-page app + .enableSingleRuntimeChunk() + + /* + * FEATURE CONFIG + * + * Enable & configure other features below. For a full + * list of features, see: + * https://symfony.com/doc/current/frontend.html#adding-more-features + */ + .cleanupOutputBeforeBuild() + + // Displays build status system notifications to the user + // .enableBuildNotifications() + + .enableSourceMaps(!Encore.isProduction()) + // enables hashed filenames (e.g. app.abc123.css) + .enableVersioning(Encore.isProduction()) + + // configure Babel + // .configureBabel((config) => { + // config.plugins.push('@babel/a-babel-plugin'); + // }) + + // enables and configure @babel/preset-env polyfills + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage'; + config.corejs = '3.38'; + }) + + // enables Sass/SCSS support + .enableSassLoader() + + // uncomment if you use TypeScript + //.enableTypeScriptLoader() + + // uncomment if you use React + //.enableReactPreset() + + // uncomment to get integrity="..." attributes on your script & link tags + // requires WebpackEncoreBundle 1.4 or higher + //.enableIntegrityHashes(Encore.isProduction()) + + // uncomment if you're having problems with a jQuery plugin + //.autoProvidejQuery() +; + +module.exports = Encore.getWebpackConfig();