diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 39784957..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,67 +0,0 @@ -# This configuration was automatically generated from a CircleCI 1.0 config. -# It should include any build commands you had along with commands that CircleCI -# inferred from your project structure. We strongly recommend you read all the -# comments in this file to understand the structure of CircleCI 2.0, as the idiom -# for configuration has changed substantially in 2.0 to allow arbitrary jobs rather -# than the prescribed lifecycle of 1.0. In general, we recommend using this generated -# configuration as a reference rather than using it in production, though in most -# cases it should duplicate the execution of your original 1.0 config. -version: 2 -jobs: - build: - working_directory: ~/vhs/nomos - parallelism: 1 - shell: /bin/bash --login - # CircleCI 2.0 does not support environment variables that refer to each other the same way as 1.0 did. - # If any of these refer to each other, rewrite them so that they don't or see https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables . - environment: - CIRCLE_ARTIFACTS: /tmp/circleci-artifacts - CIRCLE_TEST_REPORTS: /tmp/circleci-test-results - # In CircleCI 1.0 we used a pre-configured image with a large number of languages and other packages. - # In CircleCI 2.0 you can now specify your own image, or use one of our pre-configured images. - # The following configuration line tells CircleCI to use the specified docker image as the runtime environment for you job. - # We have selected a pre-built image that mirrors the build environment we use on - # the 1.0 platform, but we recommend you choose an image more tailored to the needs - # of each job. For more information on choosing an image (or alternatively using a - # VM instead of a container) see https://circleci.com/docs/2.0/executor-types/ - # To see the list of pre-built images that CircleCI provides for most common languages see - # https://circleci.com/docs/2.0/circleci-images/ - docker: - - image: cimg/php:8.3-node - steps: - - checkout - - - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS - - - restore_cache: - keys: - - v1-dep-{{ .Branch }}- - - v1-dep-master- - - v1-dep- - - - run: npm ci - - - save_cache: - key: v1-dep-{{ .Branch }}-{{ epoch }} - paths: - # This is a broad list of cache paths to include many possible development environments - # You can probably delete some of these entries - - vendor/ - - ~/virtualenvs - - ~/.m2 - - ~/.ivy2 - - ~/.bundle - - ~/.go_workspace - - ~/.gradle - - ~/.cache/bower - - - run: npm run test - - - store_test_results: - path: /tmp/circleci-test-results - - - store_artifacts: - path: /tmp/circleci-artifacts - - - store_artifacts: - path: /tmp/circleci-test-results diff --git a/.dockerignore b/.dockerignore index 6caca927..b5fc5777 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,17 @@ -.phpunit.cache/ +.env* +.git/ +.php*.cache +.phpunit.cache +.wireit/ +*.log data/ logs/ node_modules/ -tools/composer.phar -vendor/ -web/components/ -.php*.cache -.env* -*.log +packages/backend-php/docs/ +packages/backend-php/tools/composer.phar +packages/backend-php/vendor/ +packages/frontend-web/web/components/ +packages/typescript +build-cache/ +packages/*/node_modules/ +packages/*/.wireit/ diff --git a/.editorconfig b/.editorconfig index fe8d9bff..7f0cd96c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,13 @@ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file -root = true +root=true [*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -print_width = 120 +indent_style=space +indent_size=4 +end_of_line=lf +charset=utf-8 +trim_trailing_whitespace=true +insert_final_newline=true +print_width=120 diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 882d87b7..86711ae6 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -19,7 +19,7 @@ jobs: build: strategy: matrix: - component: [backend, frontend, webhooker] + component: [backend-php, frontend-react, frontend-web, webhooker] runs-on: ubuntu-latest diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000..a3c04f58 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,44 @@ +name: Run Tests + +on: + push: + branches: + - 'main' + - 'master' + - 'trunk' + tags: + - 'v*' + pull_request: + branches: + - 'main' + - 'master' + - 'trunk' + workflow_dispatch: + +jobs: + test-php: + strategy: + matrix: + package: [backend-php] + node-version: ['22'] + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{matrix.node-version}} + uses: actions/setup-node@v4 + with: + node-version: ${{matrix.node-version}} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Test ${{matrix.package}} + run: pnpm --filter "./packages/${{matrix.package}}/" test diff --git a/.gitignore b/.gitignore index 50ae5cee..5e42a6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,251 +1,101 @@ +* +!.gitignore + +!.github/ +!.github/**/ +!.github/**/* + +!.husky/ +!.husky/* + +!app/ +!app/**/ +!app/**/*.php + +!conf/ +!conf/**/ +!conf/**/*.conf +!conf/**/*.ini +!conf/**/*.php +!conf/mimes.types conf/config.ini.php -tools/rabbitmq-signing-key-public.asc* -backup -*.gz -*.zip -logs/*.log -logs/*.log.* - -################# -## Eclipse -################# - -*.pydevproject -.project -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.classpath -.settings/ -.loadpath -.idea/ - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# CDT-specific -.cproject - -# PDT-specific -.buildpath - -################# -## Visual Studio -################# - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -x64/ -build/ -[Bb]in/ -[Oo]bj/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml -*.pubxml -*.publishproj - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml -*.pfx -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - -############# -## Windows detritus -############# - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac crap -.DS_Store - -############# -## Python -############# - -*.py[cod] - -# Packages -*.egg -*.egg-info -build/ -eggs/ -parts/ -var/ -develop-eggs/ -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg - -#Vagrant -.vagrant - -#Composer -composer.phar -vendor - -#Docker env -docker/nomos.env -#Coverage -coverage.xml -coverage/. +!docker-compose/ +!docker-compose/Dockerfile +!docker-compose/*.yml + +!docker/ +!docker/dct/ +!docker/dct/**/ +!docker/dct/**/*.ts +!docker/* +!docker/nomos.env.template +docker/nomos.env -data/** -node_modules -docker-compose.conf +!migrations/ +!migrations/**/ +!migrations/**/*.sql -.vscode -conf/config.js +!packages/ +!packages/**/ -.php-cs-fixer.cache -.phpunit.cache -.phpunit.result.cache +!tests/ +!tests/**/ +!tests/**/*.php +!tools/ +!tools/**/ +!tools/backup/EMPTY +!tools/**/*.md +!tools/**/*.php +!tools/**/*.sh +!tools/**/*.ts tools/composer.phar -.wireit -docker-compose.override.yml +!vhs/ +!vhs/**/ +!vhs/**/*.php + +!web/ +!web/**/* web/components/ + +!.bowerrc +!.circleci/config.yml +!.dockerignore +!.editorconfig +!.github/workflows/build-docker.yml +!.husky/pre-commit +!.npmrc +!.php-cs-fixer.php +!.prettierignore +!.shellcheckrc +!Dockerfile +!README.md +!Vagrantfile + +!bower.json +!composer.json +!composer.lock + +!docker-compose.dev.conf +!docker-compose.sample.conf +!docker-compose.sh +!docker-compose.template.conf + +!eslint.config.mjs +!justfile +!logs/EMPTY +nomos.d.ts +!package.json +!php-ts-transformer.php +!phpstan.neon +!phpunit.xml +!pnpm-lock.yaml +!pnpm-workspace.yaml +!psalm.xml +!prettier.config.mjs +!tsconfig.json + +!diff-report.txt +!generate-diff-report.sh diff --git a/.husky/pre-commit b/.husky/pre-commit index 8b142457..4b4b14c0 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,23 +1,5 @@ #!/usr/bin/env sh -FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') +cd "$(dirname "$(realpath "$0")")/../" || exit 255 -if [ "${FILES}" != "" ]; then - PHP_FILES=$(echo "${FILES}" | grep '\.php' | xargs) - WEBHOOKER_FILES=$(echo "${FILES}" | grep 'webhooker/' | xargs) - - if [ "${PHP_FILES}" != "" ]; then - FILES=$(echo "${PHP_FILES}" | xargs) npm exec just format php - npm exec just test php - fi - - if [ "${WEBHOOKER_FILES}" != "" ]; then - npm exec just test webhooker - fi - - FILES=$(echo "${FILES}" | xargs) npm exec just format all - - git update-index --again - - exit 0 -fi +pnpm exec just git_hook_pre_commit diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..069ba621 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +cd "$(dirname "$(realpath "$0")")/../" || exit 255 + +pnpm exec just git_hook_pre_push diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..eccf7d8c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +inject-workspace-packages=true diff --git a/.prettierignore b/.prettierignore index c0f20908..9f0509e8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,26 +1,12 @@ .phpunit.cache/ -data/ -node_modules/ -vendor/ -web/components/ -package-lock.json composer.lock -web/badges/cert_cnc_mill_lathe.svg -web/badges/cert_laser.svg -web/badges/door_access.svg -web/badges/key_holder.svg -web/badges/key_holder_inactive.svg -web/badges/newsletter.svg +data/ +diff-report.txt +docker-compose.dev.conf +docker-compose.sample.conf +icons.psd justfile migrations/1/ -migrations/2/ -migrations/3/ -migrations/4/ -migrations/5/ -migrations/6/ -migrations/7/ -migrations/8/ -migrations/9/ migrations/10/ migrations/11/ migrations/12/ @@ -31,6 +17,7 @@ migrations/16/ migrations/17/ migrations/18/ migrations/19/ +migrations/2/ migrations/20/ migrations/21/ migrations/22/ @@ -38,6 +25,36 @@ migrations/23/ migrations/24/ migrations/25/ migrations/26/ -web/components/bower/datatables/media/images/Sorting icons.psd -web/components/bower/datatables/media/images/Sorting -icons.psd +migrations/3/ +migrations/4/ +migrations/5/ +migrations/6/ +migrations/7/ +migrations/8/ +migrations/9/ +node_modules/ +package-lock.json +packages/frontend-react/*.svg +packages/frontend-react/pnpm-lock.yaml +packages/frontend-react/public/assets/ +packages/frontend-react/src/routeTree.gen.ts +packages/frontend-react/src/types/nomos.d.ts +packages/frontend-web/web/badges/cert_cnc_mill_lathe.svg +packages/frontend-web/web/badges/cert_laser.svg +packages/frontend-web/web/badges/door_access.svg +packages/frontend-web/web/badges/key_holder_inactive.svg +packages/frontend-web/web/badges/key_holder.svg +packages/frontend-web/web/badges/newsletter.svg +packages/frontend-web/web/components/ +packages/frontend-web/web/components/bower/datatables/media/images/Sorting +packages/frontend-web/web/components/bower/datatables/media/images/Sorting icons.psd +packages/frontend-web/web/gateways/moneybookers/MoneyBookers_big.png +packages/frontend-web/web/gateways/moneybookers/MoneyBookers.png +packages/frontend-web/web/gateways/paypal/PayPal_big.png +packages/frontend-web/web/gateways/paypal/PayPal.png +packages/frontend-web/web/setup/img +packages/frontend-web/web/setup/license.html +packages/frontend-web/web/setup/style.css +packages/webhooker/webhooker.service +pnpm-lock.yaml +vendor/ diff --git a/README.md b/README.md index bb557458..404b84ff 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ then run `./docker-compose.sh up` again. You're all set! You can get the address to access the Nomos service from your docker host system by running the following in a separate terminal as `docker-compose.sh`: ``` -$ docker inspect nomos-frontend | jq -r '.[0].NetworkSettings.Networks | to_entries | .[0].value.IPAddress' +$ ./docker-compose.sh ps frontend -q | xargs docker inspect | jq -r '.[0].NetworkSettings.Networks | to_entries | .[0].value.IPAddress' ``` Or make your docker-compose.conf include `docker-compose/core.ports.yml`, to proxy port 80 of your docker network to port 80 on your host machine. diff --git a/app/constants/DateTime.php b/app/constants/DateTime.php deleted file mode 100644 index 3700b788..00000000 --- a/app/constants/DateTime.php +++ /dev/null @@ -1,8 +0,0 @@ -authorized, false), - OrderBy::Descending(AccessLogSchema::Columns()->time), - $limit - ); - } - - public static function log($key, $type, $authorized, $from_ip, $userid = null) { - $entry = new AccessLog(); - $entry->key = $key; - $entry->type = $type; - $entry->authorized = $authorized; - $entry->from_ip = $from_ip; - $entry->time = date(Database::DateFormat()); - $entry->userid = $userid; - $entry->save(); - - return $entry; - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - return true; - } -} diff --git a/app/domain/AccessToken.php b/app/domain/AccessToken.php deleted file mode 100644 index d15dfb0b..00000000 --- a/app/domain/AccessToken.php +++ /dev/null @@ -1,47 +0,0 @@ -token, $token), - OrderBy::Descending(AccessTokenSchema::Columns()->expires), - 1 - ); - - if (count($tokens) === 1) { - return $tokens[0]; - } - - return null; - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - return true; - } -} diff --git a/app/domain/AppClient.php b/app/domain/AppClient.php deleted file mode 100644 index 9447aaf9..00000000 --- a/app/domain/AppClient.php +++ /dev/null @@ -1,30 +0,0 @@ -code, $code)); - - if (!empty($val)) { - return $val[0]; - } - - return null; - } - - public static function generate($code, $context) { - $template = self::findByCode($code); - - if (is_null($template)) { - return null; - } - - $engine = new \StringTemplate\Engine('{{', '}}'); - - $ret = []; - - $ret['subject'] = $engine->render($template->subject, $context); - $ret['txt'] = $engine->render($template->body, $context); - $ret['html'] = $engine->render($template->html, $context); - - return $ret; - } - - public function validate(ValidationResults &$results) { - // not required at this time - } -} diff --git a/app/domain/Event.php b/app/domain/Event.php deleted file mode 100644 index c901befa..00000000 --- a/app/domain/Event.php +++ /dev/null @@ -1,41 +0,0 @@ -Columns()->domain, $domain), Where::Equal(Event::Schema()->Columns()->event, $event)) - ); - - return count($events) > 0; - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - // TODO: Implement validate() method. - } -} diff --git a/app/domain/GenuineCard.php b/app/domain/GenuineCard.php deleted file mode 100644 index 663dfd7a..00000000 --- a/app/domain/GenuineCard.php +++ /dev/null @@ -1,33 +0,0 @@ -key, $key)); - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/Ipn.php b/app/domain/Ipn.php deleted file mode 100644 index 1353e3de..00000000 --- a/app/domain/Ipn.php +++ /dev/null @@ -1,23 +0,0 @@ -type, 'api'), Where::Equal(KeySchema::Columns()->key, $key))); - } - - public static function findByPin($pin) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, 'pin'), Where::Equal(KeySchema::Columns()->key, $pin))); - } - - public static function findByRfid($rfid) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, 'rfid'), Where::Equal(KeySchema::Columns()->key, $rfid))); - } - - public static function findByService($service, $key) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, $service), Where::Equal(KeySchema::Columns()->key, $key))); - } - - public static function findByTypes(...$types) { - return self::where(Where::In(KeySchema::Columns()->type, $types)); - } - - public static function findKeyAndType($key, $type) { - return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, $type), Where::Equal(KeySchema::Columns()->key, $key))); - } - - public static function getSystemApiKeys() { - return self::where(Where::_And(Where::Null(KeySchema::Columns()->userid), Where::Equal(KeySchema::Columns()->type, 'api'))); - } - - public static function getUserApiKeys($userid) { - return self::where(Where::_And(Where::Equal(Key::Schema()->Columns()->type, 'api'), Where::Equal(Key::Schema()->Columns()->userid, $userid))); - } - - public function getAbsolutePrivileges() { - $privs = []; - - foreach ($this->privileges->all() as $priv) { - if ($priv->code === 'inherit' && $this->userid != null) { - $user = User::find($this->userid); - - if ($user != null) { - foreach ($user->privileges->all() as $userpriv) { - array_push($privs, $userpriv); - } - - if (!is_null($user->membership)) { - foreach ($user->membership->privileges->all() as $mempriv) { - array_push($privs, $mempriv); - } - } - } - } - - array_push($privs, $priv); - } - - $retval = []; - - foreach (array_unique($privs) as $priv) { - //hack array_unique may convert to object - array_push($retval, $priv); - } - - return $retval; - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/Membership.php b/app/domain/Membership.php deleted file mode 100644 index de74fcac..00000000 --- a/app/domain/Membership.php +++ /dev/null @@ -1,95 +0,0 @@ -code, $code)); - } - - /** - * @param $price - * - * @return Membership - */ - public static function findForPriceLevel($price) { - $memberships = Membership::where( - Where::_And(Where::LesserEqual(MembershipSchema::Columns()->price, $price), Where::Equal(MembershipSchema::Columns()->active, true)), - OrderBy::Descending(MembershipSchema::Columns()->price), - 1 - ); - - if (!is_null($memberships) && !empty($memberships)) { - return $memberships[0]; - } - - return null; - } - - public function validate(ValidationResults &$results) { - // not required at this time - } -} diff --git a/app/domain/PasswordResetRequest.php b/app/domain/PasswordResetRequest.php deleted file mode 100644 index fb5c2807..00000000 --- a/app/domain/PasswordResetRequest.php +++ /dev/null @@ -1,28 +0,0 @@ -token, $token)); - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/Payment.php b/app/domain/Payment.php deleted file mode 100644 index cbcd5813..00000000 --- a/app/domain/Payment.php +++ /dev/null @@ -1,32 +0,0 @@ -txn_id, $txn_id)) - ); - } - - public function validate(ValidationResults &$results) { - } -} diff --git a/app/domain/Privilege.php b/app/domain/Privilege.php deleted file mode 100644 index 21110347..00000000 --- a/app/domain/Privilege.php +++ /dev/null @@ -1,64 +0,0 @@ -Columns()->code, $code)); - - if (!empty($privs)) { - return $privs[0]; - } - - return null; - } - - public static function findByCodes(...$codes) { - if (!self::checkCodeAccess(...$codes)) { - throw new UnauthorizedException(); - } - - return Privilege::where(Where::In(Privilege::Schema()->Columns()->code, $codes)); - } - - private static function checkCodeAccess(...$codes) { - foreach ($codes as $code) { - if ( - $code != 'inherit' && - !CurrentUser::hasAllPermissions('administrator') && - !CurrentUser::hasAllPermissions($code) && - !CurrentUser::canGrantAllPermissions($code) - ) { - return false; - } - } - - return true; - } - - public function validate(ValidationResults &$results) { - // Do nothing - } -} diff --git a/app/domain/RefreshToken.php b/app/domain/RefreshToken.php deleted file mode 100644 index 74c326c0..00000000 --- a/app/domain/RefreshToken.php +++ /dev/null @@ -1,47 +0,0 @@ -token, $token), - OrderBy::Descending(RefreshTokenSchema::Columns()->expires), - 1 - ); - - if (count($tokens) === 1) { - return $tokens[0]; - } - - return null; - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - return true; - } -} diff --git a/app/domain/StripeEvent.php b/app/domain/StripeEvent.php deleted file mode 100644 index 2da03197..00000000 --- a/app/domain/StripeEvent.php +++ /dev/null @@ -1,21 +0,0 @@ -key, $key)); - - if (is_null($prefs) || count($prefs) == 0 || is_null($accessCheck)) { - return $prefs; - } - - $accessiblePrefs = []; - - /** @var SystemPreference $pref */ - foreach ($prefs as $pref) { - if ($accessCheck($pref->privileges)) { - array_push($accessiblePrefs, $pref); - } - } - - return $accessiblePrefs; - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - // TODO: Implement validate() method. - } -} diff --git a/app/domain/User.php b/app/domain/User.php deleted file mode 100644 index a957bc47..00000000 --- a/app/domain/User.php +++ /dev/null @@ -1,168 +0,0 @@ -username, $username); - $emailWhere = Where::Equal(UserSchema::Columns()->email, $email); - $where = null; - - if (!is_null($username) && !is_null($email)) { - $where = Where::_Or($usernameWhere, $emailWhere); - } elseif (!is_null($email)) { - $where = $emailWhere; - } else { - $where = $usernameWhere; - } - - return Database::exists(Query::select(UserSchema::Table(), UserSchema::Columns(), $where)); - } - - /** - * @param $email - * - * @return User[] - */ - public static function findByEmail($email) { - return User::where(Where::Equal(UserSchema::Columns()->email, $email)); - } - - /** - * @param $email - * - * @return User[] - */ - public static function findByPaymentEmail($email) { - return User::where(Where::Equal(UserSchema::Columns()->payment_email, $email)); - } - - public static function findByToken($token) { - return User::where(Where::Equal(UserSchema::Columns()->token, $token)); - } - - /** - * @param $username - * - * @return User[] - */ - public static function findByUsername($username) { - return User::where(Where::Equal(UserSchema::Columns()->username, $username)); - } - - /** - * Magic field interface method for 'valid'. - * - * @return boolean - */ - public function get_valid() { - // Check if account is active - if ($this->active != 'y') { - return false; - } - - // Check for administrator privilege - // We don't want to accidentally lock out administrators - // TODO: improve this - $privs = $this->getPrivilegeCodes(); - if (in_array('administrator', $privs)) { - return true; - } - - // check if membership has expired - return !$this->hasExpired(); - } - - public function getGrantCodes() { - $grants = []; - foreach ($this->privileges->all() as $priv) { - if (strpos($priv->code, 'grant:') === 0) { - array_push($grants, substr($priv->code, 6)); - } - } - - return $grants; - } - - /** - * Get a friendly error message for user validity. - * - * @return mixed - */ - public function getInvalidReason() { - if ($this->valid) { - return false; - } - - // Check if account is active - if ($this->active != 'y') { - return 'Account is not active'; - } - - // check if membership has expired - if ($this->hasExpired()) { - return 'Account expired'; - } - - return 'Unknown error'; - } - - public function getPrivilegeCodes() { - $codes = []; - - foreach ($this->privileges->all() as $priv) { - array_push($codes, $priv->code); - } - - return $codes; - } - - public function validate(ValidationResults &$results) { - $this->validateEmail($results); - } - - /** - * Check if user account has expired. - * - * @return boolean - */ - private function hasExpired() { - return new DateTime($this->mem_expire) < new DateTime(); - } - - private function validateEmail(ValidationResults &$results) { - if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) { - $results->add(new ValidationFailure('Invalid e-mail address')); - } - } -} diff --git a/app/domain/WebHook.php b/app/domain/WebHook.php deleted file mode 100644 index 4230fa69..00000000 --- a/app/domain/WebHook.php +++ /dev/null @@ -1,40 +0,0 @@ -Columns()->domain, $domain), Where::Equal(WebHook::Schema()->Columns()->event, $event)) - ); - } - - /** - * @param ValidationResults $results - * - * @return bool - */ - public function validate(ValidationResults &$results) { - // TODO: Implement validate() method. - } -} diff --git a/app/exceptions/InvalidInputException.php b/app/exceptions/InvalidInputException.php deleted file mode 100644 index 8c95d606..00000000 --- a/app/exceptions/InvalidInputException.php +++ /dev/null @@ -1,9 +0,0 @@ -status = $status; - $stripe_event->created = $created; - $stripe_event->event_id = $event_id; - $stripe_event->type = $type; - $stripe_event->object = $object; - $stripe_event->request = $request; - $stripe_event->api_version = $api_version; - $stripe_event->raw = $raw; - - return $stripe_event; - } - - public function Name() { - return 'stripe'; - } - - public function Process($payload) { - if (!isset($_SERVER['HTTP_STRIPE_SIGNATURE'])) { - throw new PaymentGatewayException('Error: Unknown Stripe Event Error: Missing signature'); - http_response_code(400); - exit(); - } - - $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE']; - $event = null; - - try { - $event = \Stripe\Webhook::constructEvent($payload, $sig_header, STRIPE_WEBHOOK_SECRET); - } catch (\UnexpectedValueException $e) { - // Invalid payload - throw new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); - http_response_code(400); - exit(); - } catch (\Stripe\Exception\SignatureVerificationException $e) { - // Invalid signature - throw new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); - http_response_code(400); - exit(); - } finally { - // Set up the initial event - $event_record = $this->CreateStripeEventRecord( - 'UNKNOWN', - $event->created, - $event->id, - $event->type, - $event->object, - json_encode($event->request), - $event->api_version, - $payload - ); - - // Handle the event - switch ($event->type) { - case 'invoice.paid': - $paymentIntent = $event->data->object; // contains a StripePaymentIntent - $event_record->status = 'VALID'; - $event_record->save(); - break; - default: - $event_record->save(); - throw new PaymentGatewayException('Received unknown event type ' . $event->type); - } - - http_response_code(200); - - return json_encode(['result' => 'OK']); - } - } -} diff --git a/app/include.php b/app/include.php deleted file mode 100644 index f885c77e..00000000 --- a/app/include.php +++ /dev/null @@ -1,47 +0,0 @@ -setLogger($sqlLog); - -\vhs\database\Database::setEngine($mySqlEngine); - -$rabbitLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/rabbit.log') : new \vhs\loggers\SilentLogger(); - -\vhs\messaging\MessageQueue::setLogger($rabbitLog); -\vhs\messaging\MessageQueue::setRethrow(true); - -$rabbitMQ = new \vhs\messaging\engines\RabbitMQ\RabbitMQEngine( - new \vhs\messaging\engines\RabbitMQ\RabbitMQConnectionInfo(RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USER, RABBITMQ_PASSWORD, RABBITMQ_VHOST) -); - -$rabbitMQ->setLogger($rabbitLog); - -\vhs\messaging\MessageQueue::setEngine($rabbitMQ); - -\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', ROOT_NAMESPACE_PATH)); - -$serviceLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/service.log') : new \vhs\loggers\SilentLogger(); - -\vhs\services\ServiceRegistry::register($serviceLog, 'web', 'app\\endpoints\\web', ROOT_NAMESPACE_PATH); -\vhs\services\ServiceRegistry::register($serviceLog, 'native', 'app\\endpoints\\native', ROOT_NAMESPACE_PATH); diff --git a/app/schema/AccessTokenSchema.php b/app/schema/AccessTokenSchema.php deleted file mode 100644 index 2a9d0b89..00000000 --- a/app/schema/AccessTokenSchema.php +++ /dev/null @@ -1,38 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('token', Type::String()); - $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('userid', Type::Int()); - $table->addColumn('appclientid', Type::Int()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->appclientid, AppClientSchema::Table(), AppClientSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table)); - - return $table; - } -} diff --git a/app/schema/AppClientSchema.php b/app/schema/AppClientSchema.php deleted file mode 100644 index 25830e49..00000000 --- a/app/schema/AppClientSchema.php +++ /dev/null @@ -1,41 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('secret', Type::String()); - $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('userid', Type::Int()); - $table->addColumn('name', Type::String()); - $table->addColumn('description', Type::String()); - $table->addColumn('url', Type::String()); - $table->addColumn('redirecturi', Type::String()); - $table->addColumn('enabled', Type::Bool(false, false)); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('appclient', $table, $table->columns->userid)); - - return $table; - } -} diff --git a/app/schema/EventPrivilegeSchema.php b/app/schema/EventPrivilegeSchema.php deleted file mode 100644 index 36421564..00000000 --- a/app/schema/EventPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('eventid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->eventid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->eventid, EventSchema::Table(), EventSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/KeyPrivilegeSchema.php b/app/schema/KeyPrivilegeSchema.php deleted file mode 100644 index 7484034e..00000000 --- a/app/schema/KeyPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('keyid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->keyid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->keyid, KeySchema::Table(), KeySchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/KeySchema.php b/app/schema/KeySchema.php deleted file mode 100644 index d9189d30..00000000 --- a/app/schema/KeySchema.php +++ /dev/null @@ -1,39 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('userid', Type::Int()); - $table->addColumn('type', Type::Enum('undefined', 'api', 'rfid', 'pin', 'github', 'google', 'slack')); - $table->addColumn('key', Type::String(true, null, 255)); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - $table->addColumn('expires', Type::DateTime()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('key', $table, $table->columns->userid)); - - return $table; - } -} diff --git a/app/schema/MembershipPrivilegeSchema.php b/app/schema/MembershipPrivilegeSchema.php deleted file mode 100644 index ac8795c3..00000000 --- a/app/schema/MembershipPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('membershipid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->membershipid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->membershipid, MembershipSchema::Table(), MembershipSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/PasswordResetRequestSchema.php b/app/schema/PasswordResetRequestSchema.php deleted file mode 100644 index 1950633d..00000000 --- a/app/schema/PasswordResetRequestSchema.php +++ /dev/null @@ -1,36 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('userid', Type::Int()); - $table->addColumn('token', Type::String(true, null, 255)); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('passwordresetrequest', $table, $table->columns->userid)); - - return $table; - } -} diff --git a/app/schema/PaymentSchema.php b/app/schema/PaymentSchema.php deleted file mode 100644 index eefa4d83..00000000 --- a/app/schema/PaymentSchema.php +++ /dev/null @@ -1,50 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('txn_id', Type::String(false, '', 100)); //txn_id - $table->addColumn('membership_id', Type::Int(true, 0)); - $table->addColumn('user_id', Type::Int(true, 0)); - $table->addColumn('payer_email', Type::String(true, null, 255)); - $table->addColumn('payer_fname', Type::String(true, null, 255)); - $table->addColumn('payer_lname', Type::String(true, null, 255)); - $table->addColumn('rate_amount', Type::String(false, '', 255)); - $table->addColumn('currency', Type::String(true, null, 4)); - $table->addColumn('date', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('pp', Type::Enum('PayPal', 'MoneyBookers', 'Stripe')); - $table->addColumn('ip', Type::String(true, null, 20)); - $table->addColumn('status', Type::Int(false, 0)); // 1==completed, anything else is "pending" - $table->addColumn('item_name', Type::String(true, null, 255)); - $table->addColumn('item_number', Type::String(true, null, 255)); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->membership_id, MembershipSchema::Table(), MembershipSchema::Columns()->id), - Constraint::ForeignKey($table->columns->user_id, UserSchema::Table(), UserSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('payment', $table, $table->columns->user_id)); - - return $table; - } -} diff --git a/app/schema/RefreshTokenSchema.php b/app/schema/RefreshTokenSchema.php deleted file mode 100644 index 71d79994..00000000 --- a/app/schema/RefreshTokenSchema.php +++ /dev/null @@ -1,38 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('token', Type::String()); - $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('userid', Type::Int()); - $table->addColumn('appclientid', Type::Int()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->appclientid, AppClientSchema::Table(), AppClientSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table, $table->columns->userid)); - - return $table; - } -} diff --git a/app/schema/SystemPreferencePrivilegeSchema.php b/app/schema/SystemPreferencePrivilegeSchema.php deleted file mode 100644 index 616b8832..00000000 --- a/app/schema/SystemPreferencePrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('systempreferenceid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->systempreferenceid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->systempreferenceid, SystemPreferenceSchema::Table(), SystemPreferenceSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/UserPrivilegeSchema.php b/app/schema/UserPrivilegeSchema.php deleted file mode 100644 index 446cdd08..00000000 --- a/app/schema/UserPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('userid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->userid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/UserSchema.php b/app/schema/UserSchema.php deleted file mode 100644 index aa919d21..00000000 --- a/app/schema/UserSchema.php +++ /dev/null @@ -1,56 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('username', Type::String(false, '', 255)); - $table->addColumn('password', Type::String(false, '', 255), false); - $table->addColumn('membership_id', Type::Int(false, 0)); - $table->addColumn('mem_expire', Type::DateTime(true, date('Y-m-d H:i:s'))); - $table->addColumn('trial_used', Type::Bool(false, false)); - $table->addColumn('email', Type::String(false, '', 255)); - $table->addColumn('fname', Type::String(false, '', 32)); - $table->addColumn('lname', Type::String(false, '', 32)); - $table->addColumn('token', Type::String(false, '0', 40)); - $table->addColumn('cookie_id', Type::String(false, '0', 64)); - $table->addColumn('newsletter', Type::Bool(false, false)); - $table->addColumn('cash', Type::Bool(false, false)); - $table->addColumn('userlevel', Type::Int(false, 1)); - $table->addColumn('notes', Type::Text()); - $table->addColumn('created', Type::DateTime(true, date('Y-m-d H:i:s'))); - $table->addColumn('lastlogin', Type::DateTime(true, date('Y-m-d H:i:s'))); - $table->addColumn('lastip', Type::String(true, '0', 16)); - $table->addColumn('avatar', Type::String(true, '0', 150)); - $table->addColumn('active', Type::Enum('n', 'y', 't', 'b')); - $table->addColumn('paypal_id', Type::String(false, '', 255)); - $table->addColumn('payment_email', Type::String(false, '', 255)); - $table->addColumn('stripe_id', Type::String(false, '', 255)); - $table->addColumn('stripe_email', Type::String(false, '', 255)); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->membership_id, MembershipSchema::Table(), MembershipSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('user', $table, $table->columns->id)); - - return $table; - } -} diff --git a/app/schema/WebHookPrivilegeSchema.php b/app/schema/WebHookPrivilegeSchema.php deleted file mode 100644 index d83c5903..00000000 --- a/app/schema/WebHookPrivilegeSchema.php +++ /dev/null @@ -1,35 +0,0 @@ -addColumn('webhookid', Type::Int()); - $table->addColumn('privilegeid', Type::Int()); - $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); - $table->addColumn('notes', Type::Text()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->webhookid), - Constraint::PrimaryKey($table->columns->privilegeid), - Constraint::ForeignKey($table->columns->webhookid, WebHookSchema::Table(), WebHookSchema::Columns()->id), - Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) - ); - - return $table; - } -} diff --git a/app/schema/WebHookSchema.php b/app/schema/WebHookSchema.php deleted file mode 100644 index 2d46bdb8..00000000 --- a/app/schema/WebHookSchema.php +++ /dev/null @@ -1,46 +0,0 @@ -addColumn('id', Type::Int(false, 0)); - $table->addColumn('name', Type::String(false, '', 255)); - $table->addColumn('description', Type::Text()); - $table->addColumn('enabled', Type::Bool(false, false)); - $table->addColumn('userid', Type::Int()); - $table->addColumn('url', Type::String(false, '', 255)); - $table->addColumn('translation', Type::Text()); - $table->addColumn('headers', Type::Text()); - $table->addColumn('method', Type::String(false, 'POST', 32)); - $table->addColumn('eventid', Type::Int()); - - $table->setConstraints( - Constraint::PrimaryKey($table->columns->id), - Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), - Constraint::ForeignKey($table->columns->eventid, EventSchema::Table(), EventSchema::Columns()->id) - ); - - $table->setAccess(PrivilegedAccess::GenerateAccess('webhook', $table, $table->columns->userid)); - - return $table; - } -} diff --git a/app/security/Authenticate.php b/app/security/Authenticate.php deleted file mode 100644 index a59de1c9..00000000 --- a/app/security/Authenticate.php +++ /dev/null @@ -1,310 +0,0 @@ -getUsername(), $credentials->getPassword()); - break; - case 'app\\security\\credentials\\ApiCredentials': - /** @var ApiCredentials $credentials */ - self::keyLogin(Key::findByApiKey($credentials->getToken()), $credentials); - break; - case 'app\\security\\credentials\\RfidCredentials': - /** @var RfidCredentials $credentials */ - self::keyLogin(Key::findByRfid($credentials->getToken()), $credentials); - break; - case 'app\\security\\credentials\\PinCredentials': - /** @var PinCredentials $credentials */ - self::keyLogin(Key::findByPin($credentials->getToken()), $credentials); - break; - case 'vhs\\security\\BearerTokenCredentials': - /** @var BearerTokenCredentials $credentials */ - self::bearerLogin($credentials); - break; - default: - throw new InvalidCredentials('"Unsupported authentication type."'); - } - } - - public static function logout() { - CurrentUser::setPrincipal(new AnonPrincipal()); - } - - private static function bearerLogin(BearerTokenCredentials $credentials) { - $ipaddr = self::getRemoteIP(); - - $token = null; - - try { - $token = AccessToken::findByToken($credentials->getToken()); - } catch (\Exception $ex) { - // no further action required - } - - if (is_null($token) || is_null($token->user)) { - AccessLog::log($credentials->getToken(), 'bearer', false, $ipaddr); - throw new InvalidCredentials(message: '"Invalid access token"'); - } - - if ( - self::isUserValid($token->user) && - (is_null($token->client) || - ($token->client->enabled && (is_null($token->client->expires) || new DateTime($token->client->expires) > new DateTime()))) - ) { - CurrentUser::setPrincipal(self::buildPrincipal($token->user)); - - self::recordLogin($token->user, $ipaddr); - - AccessLog::log($credentials->getToken(), 'bearer', true, $ipaddr, $token->user->id); - } else { - AccessLog::log($credentials->getToken(), 'bearer', false, $ipaddr, $token->user->id); - throw new InvalidCredentials('"Invalid access token"'); - } - } - - /** - * @param $user - * - * @return UserPrincipal - */ - private static function buildPrincipal($user) { - $membershipPrivs = []; - $privileges = []; - $grants = []; - - if ($user->valid) { - if (!is_null($user->membership)) { - $membershipPrivs = array_map(function ($privilege) { - return $privilege->code; - }, $user->membership->privileges->all()); - } - - $privileges = array_merge( - $membershipPrivs, - array_map(function ($privilege) { - return $privilege->code; - }, $user->privileges->all()) - ); - - foreach ($privileges as $priv) { - if (strpos($priv, 'grant:') === 0) { - array_push($grants, substr($priv, 6)); - } - } - - if (count($grants) > 0) { - array_push($privileges, 'grants'); - } - } - - array_push($privileges, 'user'); - - return new UserPrincipal($user->id, $privileges, $grants, $user->username); - } - - /** - * @param $username - * - * @return User - * - * @throws InvalidCredentials - */ - private static function findUser($username) { - $users = User::findByUsername($username); - - if (count($users) != 1) { - //Try e-mail Address - $users = User::findByEmail($username); - } - - if (count($users) != 1) { - throw new InvalidCredentials('"Incorrect username or password"'); - } - - return $users[0]; - } - - private static function getRemoteIP() { - $ipaddr = null; - if (isset($_SERVER) && array_key_exists('REMOTE_ADDR', $_SERVER)) { - $ipaddr = $_SERVER['REMOTE_ADDR']; - } - - return $ipaddr; - } - - private static function isUserValid($user) { - switch ($user->active) { - case 'n': //not active - throw new InvalidCredentials('"Your account is not activated"'); - break; - case 'y': //yes they are active - return true; - break; - case 't': //pending email verification - throw new InvalidCredentials('"You need to verify your email address"'); - break; - case 'b': //banned - throw new InvalidCredentials('"Your account has been banned"'); - break; - } - - return false; - } - - private static function keyLogin($keys, TokenCredentials $credentials) { - $ipaddr = self::getRemoteIP(); - - if (count($keys) != 1) { - AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr); - throw new InvalidCredentials('"Invalid key"'); - } - - $key = $keys[0]; - $identity = null; - $name = 'token:' . $key->id . ':'; - - $privileges = array_map(function ($priviledge) { - return $priviledge->code; - }, $key->privileges->all()); - - if (!is_null($key->userid) && $key->userid != '0') { - try { - $user = User::find($key->userid); - } catch (\Exception $ex) { - AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr, $key->userid); - throw new InvalidCredentials('"Invalid key"'); - } - - if (!is_null($user) && self::isUserValid($user)) { - $identity = $user->id; - $name .= $user->username; - - if (in_array('inherit', $privileges)) { - array_push($privileges, 'user'); - $privileges = array_merge( - $privileges, - array_map(function ($privilege) { - return $privilege->code; - }, $user->membership->privileges->all()), - array_map(function ($privilege) { - return $privilege->code; - }, $user->privileges->all()) - ); - } - } else { - AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr); - throw new InvalidCredentials('"Invalid key"'); - } - } - - $grants = []; - foreach ($privileges as $priv) { - if (strpos($priv, 'grant:') === 0) { - array_push($grants, substr($priv, 6)); - } - } - - if (count($grants) > 0) { - array_push($privileges, 'grants'); - } - - CurrentUser::setPrincipal(new TokenPrincipal($identity, $privileges, $grants, $name)); - - AccessLog::log($credentials->getToken(), $credentials->getType(), true, $ipaddr, $key->userid); - } - - /** - * @param $user - * @param $ipaddr - * - * @throws \Exception - */ - private static function recordLogin($user, $ipaddr) { - $user->lastlogin = date(Database::DateFormat()); - $user->lastip = $ipaddr; - - try { - $user->save(); - } catch (\Exception $ex) { - self::logout(); - - throw $ex; - } - } - - private static function userLogin($username, $password, $authonly = false) { - $ipaddr = self::getRemoteIP(); - - try { - $user = self::findUser($username); - } catch (\Exception $ex) { - AccessLog::log($username, 'userpass', false, $ipaddr); - throw $ex; - } - - if (self::isUserValid($user) && PasswordUtil::check($password, $user->password)) { - if (!$authonly) { - CurrentUser::setPrincipal(self::buildPrincipal($user)); - } - - self::recordLogin($user, $ipaddr); - - AccessLog::log($username, 'userpass', true, $ipaddr, $user->id); - - return $user; - } else { - AccessLog::log($username, 'userpass', false, $ipaddr, $user->id); - throw new InvalidCredentials('"Incorrect username or password"'); - } - } -} diff --git a/app/security/ColumnPrivilegedAccess.php b/app/security/ColumnPrivilegedAccess.php deleted file mode 100644 index c0401f78..00000000 --- a/app/security/ColumnPrivilegedAccess.php +++ /dev/null @@ -1,68 +0,0 @@ -column = $column; - $this->privileges = $privileges; - } - - public function CanRead($record, Table $table, Column $column) { - return $column === $this->column && $this->hasPrivilegedAccess($record, ...$this->privileges); - } - - public function CanWrite($record, Table $table, Column $column) { - return $column === $this->column && $this->hasPrivilegedAccess($record, ...$this->privileges); - } - - public function jsonSerialize(): mixed { - return [ - 'type' => 'column', - 'column' => [ - 'table' => $this->column->table->name, - 'name' => $this->column->name, - 'type' => $this->column->type - ], - 'privileges' => $this->privileges, - 'checks' => $this->checks - ]; - } - - public function serialize(): mixed { - return [ - 'type' => 'column', - 'column' => [ - 'table' => $this->column->table->name, - 'name' => $this->column->name, - 'type' => $this->column->type - ], - 'privileges' => $this->privileges, - 'checks' => $this->checks - ]; - } - - public function __serialize() { - return $this->serialize(); - } - - public function __unserialize($data): void { - // TODO maybe implement? - } -} diff --git a/app/security/HttpApiAuthModule.php b/app/security/HttpApiAuthModule.php deleted file mode 100644 index 9acdc5b5..00000000 --- a/app/security/HttpApiAuthModule.php +++ /dev/null @@ -1,49 +0,0 @@ -authorizer = $authorizer; - } - - public function endResponse(HttpServer $server) { - if (array_key_exists('X-Api-Key', $server->request->headers) && $this->authorizer->isAuthenticated()) { - $this->authorizer->logout(); - } - } - - public function handle(HttpServer $server) { - if (array_key_exists('X-Api-Key', $server->request->headers) && !$this->authorizer->isAuthenticated()) { - try { - $this->authorizer->login(new ApiCredentials($server->request->headers['X-Api-Key'])); - } catch (\Exception $ex) { - throw new UnauthorizedException($ex->getMessage()); - } - } - } - - public function handleException(HttpServer $server, \Exception $ex) { - if (get_class($ex) === 'vhs\\security\\exceptions\\UnauthorizedException') { - $server->clear(); - $server->header('HTTP/1.0 401 Unauthorized'); - $server->code(401); - $server->end(); - } - } -} diff --git a/app/security/PasswordUtil.php b/app/security/PasswordUtil.php deleted file mode 100644 index 1f6e6a19..00000000 --- a/app/security/PasswordUtil.php +++ /dev/null @@ -1,50 +0,0 @@ -ownerColumn = $ownerColumn; - $this->checks = []; - } - - public static function GenerateAccess($key, Table $table, Column $ownerColumn = null) { - $access = null; - $child = null; - - if (is_null($ownerColumn)) { - $access = new TablePrivilegedAccess(null, $table, 'access:' . $key); - $child = $access; - } else { - $access = new PrivilegedAccess($ownerColumn); - $child = $access->Table($table, 'access:' . $key); - } - foreach ($table->columns->all() as $column) { - $child->Column($column, 'access:' . $key, 'access:' . $key . ':' . $column->name); - } - - return $access; - } - - public function CanRead($record, Table $table, Column $column) { - $access = false; - foreach ($this->checks as $check) { - $access &= $check->CanRead($record, $table, $column); - } - - return $access; - } - - public function CanWrite($record, Table $table, Column $column) { - $access = false; - foreach ($this->checks as $check) { - $access &= $check->CanWrite($record, $table, $column); - } - - return $access; - } - - public function Column(Column $column, ...$privileges) { - $access = new ColumnPrivilegedAccess($this->ownerColumn, $column, ...$privileges); - $this->Register($access); - - return $access; - } - - public function hasPrivilegedAccess($record, ...$privileges) { - if (CurrentUser::hasAnyPermissions('administrator')) { - return true; - } - - if (in_array('owner', $privileges) && $this->IsOwner($record)) { - return true; - } - - return CurrentUser::hasAnyPermissions($privileges); - } - - public function IsOwner($record) { - return array_key_exists($this->ownerColumn->name, $record) && $record[$this->ownerColumn->name] === CurrentUser::getIdentity(); - } - - /** - * Specify data which should be serialized to JSON. - * - * @link http://php.net/manual/en/jsonserializable.jsonserialize.php - * - * @return mixed data which can be serialized by json_encode, - * which is a value of any type other than a resource - * - * @since 5.4.0 - */ - public function jsonSerialize(): mixed { - return [ - 'type' => 'ownership', - 'ownership' => [ - 'table' => $this->ownerColumn->table->name, - 'name' => $this->ownerColumn->name, - 'type' => $this->ownerColumn->type - ], - 'checks' => $this->checks - ]; - } - - public function Register(IAccess ...$checks) { - foreach ($checks as $check) { - array_push($this->checks, $check); - } - } - - /** - * String representation of object. - * - * @link http://php.net/manual/en/serializable.serialize.php - * - * @return string the string representation of the object or null - * - * @since 5.1.0 - */ - public function serialize(): mixed { - return [ - 'type' => 'ownership', - 'ownership' => [ - 'table' => $this->ownerColumn->table->name, - 'name' => $this->ownerColumn->name, - 'type' => $this->ownerColumn->type - ], - 'checks' => $this->checks - ]; - } - - public function Table(Table $table, ...$privileges) { - $access = new TablePrivilegedAccess($this->ownerColumn, $table, ...$privileges); - $this->Register($access); - - return $access; - } - - /** - * Constructs the object. - * - * @link http://php.net/manual/en/serializable.unserialize.php - * - * @param string $serialized

- * The string representation of the object. - *

- * - * @return void - * - * @since 5.1.0 - */ - public function unserialize($serialized) { - // TODO: Implement unserialize() method. - } - - public function __serialize() { - return $this->serialize(); - } - - public function __unserialize($data): void { - // TODO: Implement __unserialize() method. - } -} diff --git a/app/security/TablePrivilegedAccess.php b/app/security/TablePrivilegedAccess.php deleted file mode 100644 index dd5ef136..00000000 --- a/app/security/TablePrivilegedAccess.php +++ /dev/null @@ -1,60 +0,0 @@ -table = $table; - $this->privileges = $privileges; - } - - public function CanRead($record, Table $table, Column $column) { - return $table === $this->table && $this->hasPrivilegedAccess($record, ...$this->privileges); - } - - public function CanWrite($record, Table $table, Column $column) { - return $table === $this->table && $this->hasPrivilegedAccess($record, ...$this->privileges); - } - - public function jsonSerialize(): mixed { - return [ - 'type' => 'table', - 'table' => $this->table->name, - 'privileges' => $this->privileges, - 'checks' => $this->checks - ]; - } - - public function serialize(): mixed { - return [ - 'type' => 'table', - 'table' => $this->table->name, - 'privileges' => $this->privileges, - 'checks' => $this->checks - ]; - } - - public function __serialize() { - return $this->serialize(); - } - - public function __unserialize($data): void { - // TODO maybe implement? - } -} diff --git a/app/security/TokenPrincipal.php b/app/security/TokenPrincipal.php deleted file mode 100644 index dcde6cd1..00000000 --- a/app/security/TokenPrincipal.php +++ /dev/null @@ -1,54 +0,0 @@ -id = $id; - $this->permissions = $permissions; - $this->grants = $grants; - $this->name = $name; - } - - public function canGrantAllPermissions(...$permission) { - return in_array('*', $this->grants) || count(array_diff($permission, $this->grants)) == 0; - } - - public function canGrantAnyPermissions(...$permission) { - return in_array('*', $this->grants) || count(array_intersect($permission, $this->grants)) > 0; - } - - public function getIdentity() { - return $this->id; - } - - public function hasAllPermissions(...$permission) { - return count(array_diff($permission, $this->permissions)) == 0; - } - - public function hasAnyPermissions(...$permission) { - return count(array_intersect($permission, $this->permissions)) > 0; - } - - public function isAnon() { - return false; - } - - public function __toString() { - return $this->name; - } -} diff --git a/app/security/UserPrincipal.php b/app/security/UserPrincipal.php deleted file mode 100644 index c2983593..00000000 --- a/app/security/UserPrincipal.php +++ /dev/null @@ -1,62 +0,0 @@ -id = $id; - $this->permissions = $permissions; - $this->grants = $grants; - $this->username = $username; - } - - public function canGrantAllPermissions(...$permission) { - return in_array('*', $this->grants) || count(array_diff($permission, $this->grants)) == 0; - } - - public function canGrantAnyPermissions(...$permission) { - return in_array('*', $this->grants) || count(array_intersect($permission, $this->grants)) > 0; - } - - public function getIdentity() { - return $this->id; - } - - public function hasAllPermissions(...$permission) { - return count(array_diff($permission, $this->permissions)) == 0; - } - - public function hasAnyPermissions(...$permission) { - return count(array_intersect($permission, $this->permissions)) > 0; - } - - public function isAnon() { - return false; - } - - public function jsonSerialize(): mixed { - $data = []; - $data['id'] = $this->id; - $data['permissions'] = $this->permissions; - - return $data; - } - - public function __toString() { - return 'user:' . $this->username; - } -} diff --git a/app/security/credentials/TokenCredentials.php b/app/security/credentials/TokenCredentials.php deleted file mode 100644 index f3dcc2fe..00000000 --- a/app/security/credentials/TokenCredentials.php +++ /dev/null @@ -1,26 +0,0 @@ -token = $token; - } - - abstract public function getType(); - - public function getToken() { - return $this->token; - } -} diff --git a/app/security/oauth/OAuthHelper.php b/app/security/oauth/OAuthHelper.php deleted file mode 100644 index cd4b7993..00000000 --- a/app/security/oauth/OAuthHelper.php +++ /dev/null @@ -1,92 +0,0 @@ -provider = $provider; - $this->userDetails = null; - $this->server = $server; - } - - public static function redirectHost() { - $protocol = - defined('NOMOS_FORCE_HTTPS') || ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443) - ? StringLiterals::HTTPS_PREFIX - : StringLiterals::HTTP_PREFIX; - $domainName = $_SERVER['HTTP_HOST']; - - return $protocol . $domainName; - } - - public function linkAccount($serviceUID, $serviceType, $notes) { - if (!Authenticate::isAuthenticated()) { - print 'Not logged in'; - exit(); - } - - //Update old keys even if they are assigned to other users - $keys = Key::findKeyAndType($serviceUID, $serviceType); - - if (!empty($keys)) { - $key = $keys[0]; - } else { - $key = new Key(); - } - - $key->key = $serviceUID; - $key->userid = CurrentUser::getIdentity(); - $key->type = $serviceType; - $key->notes = $notes; - - $key->privileges->clear(); - $key->save(); - - $key->privileges->add(Privilege::findByCode('inherit')); - - $key->save(); - } - - public function processToken() { - $token = $this->provider->getAccessToken('authorization_code', [ - 'code' => $_GET['code'] - ]); - if (!is_null($token)) { - $this->userDetails = $this->provider->getResourceOwner($token); - - return $this->userDetails; - } - - return null; - } - - public function requestAuth(array $options = []) { - // If we don't have an authorization code then get one - $authUrl = $this->provider->getAuthorizationUrl($options); - - $this->server->clear(); - $this->server->redirect($authUrl); - $this->server->end(); - } -} diff --git a/app/security/oauth/providers/slack/Slack.php b/app/security/oauth/providers/slack/Slack.php deleted file mode 100644 index b652b337..00000000 --- a/app/security/oauth/providers/slack/Slack.php +++ /dev/null @@ -1,61 +0,0 @@ -domain . '/api/oauth.v2.access'; - } - - public function getBaseAuthorizationUrl() { - return $this->domain . '/oauth/v2/authorize'; - } - - public function getResourceOwnerDetailsUrl(AccessToken $token) { - return $this->domain . '/api/auth.test?token=' . $token; - } - - protected function checkResponse(ResponseInterface $response, $data) { - if (isset($data['ok']) && $data['ok'] !== true) { - return SlackProviderException::fromResponse($response, $data['error']); - } - } - - protected function createAccessToken(array $response, AbstractGrant $grant) { - if (isset($response['authed_user'])) { - return new AccessToken($response['authed_user']); - } - - return new AccessToken($response['authed_user']); - } - - protected function getDefaultScopes() { - return []; - } -} diff --git a/app/security/oauth/providers/slack/SlackProviderException.php b/app/security/oauth/providers/slack/SlackProviderException.php deleted file mode 100644 index 4c121621..00000000 --- a/app/security/oauth/providers/slack/SlackProviderException.php +++ /dev/null @@ -1,12 +0,0 @@ -getStatusCode(), (string) $response->getBody()); - } -} diff --git a/app/security/oauth/providers/slack/SlackUser.php b/app/security/oauth/providers/slack/SlackUser.php deleted file mode 100644 index f4efa860..00000000 --- a/app/security/oauth/providers/slack/SlackUser.php +++ /dev/null @@ -1,25 +0,0 @@ -response = $response; - } - - public function getId() { - $this->response['user_id']; - } - - public function getName(): string { - return $this->response['user']; - } - - public function toArray() { - return $this->response; - } -} diff --git a/app/services/EmailService.php b/app/services/EmailService.php deleted file mode 100644 index c2a0747b..00000000 --- a/app/services/EmailService.php +++ /dev/null @@ -1,224 +0,0 @@ -delete(); - } - } - - public function Email($email, $tmpl, $context, $subject = null) { - $generated = EmailTemplate::generate($tmpl, $context); - - if (is_null($generated)) { - throw new \Exception('Unable to load e-mail template'); - } - - if (is_null($subject)) { - $subject = $generated['subject']; - } - - $client = new SesClient([ - 'region' => AWS_SES_REGION, - 'credentials' => [ - 'key' => AWS_SES_CLIENT_ID, - 'secret' => AWS_SES_SECRET - ] - ]); - - $client->sendEmail([ - 'Source' => NOMOS_FROM_EMAIL, - 'Destination' => [ - 'ToAddresses' => [$email] - ], - 'Message' => [ - 'Subject' => [ - // Data is required - 'Data' => $subject - ], - // Body is required - 'Body' => [ - 'Text' => [ - // Data is required - 'Data' => $generated['txt'] - ], - 'Html' => [ - // Data is required - 'Data' => $generated['html'] - ] - ] - ] - ]); - - return null; - } - - public function EmailUser($user, $tmpl, $context, $subject = null) { - $this->Email($user->email, $tmpl, $context, $subject); - } - - /** - * @permission administrator - * - * @param $id - * - * @return mixed - */ - public function GetTemplate($id) { - return EmailTemplate::find($id); - } - - /** - * @permission administrator - * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * - * @return mixed - */ - public function ListTemplates($page, $size, $columns, $order, $filters) { - return EmailTemplate::page($page, $size, $columns, $order, $filters); - } - - /** - * @permission administrator - * - * @param $name - * @param $code - * @param $subject - * @param $help - * @param $body - * @param $html - * - * @return mixed - */ - public function PutTemplate($name, $code, $subject, $help, $body, $html) { - $template = EmailTemplate::findByCode($code); - - if (is_null($template)) { - $template = new EmailTemplate(); - } - - $template->name = $name; - $template->code = $code; - $template->subject = $subject; - $template->help = $help; - $template->body = $body; - $template->html = $html; - - $template->save(); - } - - /** - * @permission administrator - * - * @param $id - * @param $body - * - * @return mixed - */ - public function UpdateTemplateBody($id, $body) { - $template = EmailTemplate::find($id); - $template->body = $body; - $template->save(); - } - - /** - * @permission administrator - * - * @param $id - * @param $code - * - * @return mixed - */ - public function UpdateTemplateCode($id, $code) { - $template = EmailTemplate::find($id); - $template->code = $code; - $template->save(); - } - - /** - * @permission administrator - * - * @param $id - * @param $help - * - * @return mixed - */ - public function UpdateTemplateHelp($id, $help) { - $template = EmailTemplate::find($id); - $template->help = $help; - $template->save(); - } - - /** - * @permission administrator - * - * @param $id - * @param $html - * - * @return mixed - */ - public function UpdateTemplateHtml($id, $html) { - $template = EmailTemplate::find($id); - $template->html = $html; - $template->save(); - } - - /** - * @permission administrator - * - * @param $id - * @param $name - * - * @return mixed - */ - public function UpdateTemplateName($id, $name) { - $template = EmailTemplate::find($id); - $template->name = $name; - $template->save(); - } - - /** - * @permission administrator - * - * @param $id - * @param $subject - * - * @return mixed - */ - public function UpdateTemplateSubject($id, $subject) { - $template = EmailTemplate::find($id); - $template->subject = $subject; - $template->save(); - } -} diff --git a/app/services/IpnService.php b/app/services/IpnService.php deleted file mode 100644 index 47ceca44..00000000 --- a/app/services/IpnService.php +++ /dev/null @@ -1,55 +0,0 @@ -addUserIDToFilters($userid, $filters); - - return GenuineCard::count($filters); - } - - /** - * @permission administrator - * - * @param $key - * - * @return mixed - */ - public function GetGenuineCardDetails($key) { - return GenuineCard::findByKey($key)[0]; - } - - /** - * @permission administrator - * - * @param $email - * @param $key - * - * @return mixed - * - * @throws \Exception - */ - public function IssueCard($email, $key) { - $users = User::findByPaymentEmail($email); - - if (is_null($users) || count($users) != 1) { - throw new InvalidInputException('Invalid email address'); - } - - if (!$this->ValidateGenuineCard($key)) { - throw new InvalidInputException('Invalid card'); - } - - $user = $users[0]; - $card = GenuineCard::findByKey($key)[0]; - - $payments = Payment::where( - Where::_And( - Where::Equal(Payment::Schema()->Columns()->status, 1), - Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), //TODO eventually put these into card campaigns or something - Where::Equal(Payment::Schema()->Columns()->payer_email, $email), - Where::Equal(Payment::Schema()->Columns()->user_id, $user->id), - Where::NotIn( - Payment::Schema()->Columns()->id, - Query::Select(GenuineCard::Schema()->Table(), new Columns(GenuineCard::Schema()->Columns()->paymentid)) - ) - ) - ); - - if (is_null($payments) || count($payments) < 1) { - throw new MemberCardException('User has not paid for a member card.'); - } - - $payment = $payments[0]; - - $card->paymentid = $payment->id; - $card->active = true; - $card->userid = $user->id; - $card->owneremail = $email; - $card->issued = date('Y-m-d H:i:s'); - $card->notes = 'Issued by admin to ' . $user->fname . ' ' . $user->lname; - - $card->save(); - - $keyService = new KeyService(); - - $keyService->GenerateUserKey($user->id, 'rfid', $key, 'Genuine VHS Membership Card'); - - return $card; - } - - /** - * @permission administrator - * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * - * @return mixed - */ - public function ListGenuineCards($page, $size, $columns, $order, $filters) { - return GenuineCard::page($page, $size, $columns, $order, $filters); - } - - /** - * @permission administrator|user - * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * - * @return mixed - * - * @throws \Exception - */ - public function ListUserGenuineCards($userid, $page, $size, $columns, $order, $filters) { - $userService = new UserService(); - $user = $userService->GetUser($userid); - - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - - if (is_null($user)) { - throw new UnauthorizedException('User not found or you do not have access'); - } - - $userFilter = Filter::_Or(Filter::Equal('userid', $user->id), Filter::Equal('owneremail', $user->email)); - - if (is_null($filters) || $filters == '') { - $filters = $userFilter; - } else { - $filters = Filter::_And($userFilter, $filters); - } - - return GenuineCard::page($page, $size, $columns, $order, $filters); - } - - /** - * @permission administrator - * - * @param $key - * @param $notes - * - * @return GenuineCard - * - * @throws \Exception - */ - public function RegisterGenuineCard($key, $notes) { - $keys = GenuineCard::findByKey($key); - - if (!is_null($keys) && count($keys) != 0) { - //card already registered - throw new MemberCardException('Failed to register card'); - } - - $card = new GenuineCard(); - - $card->key = $key; - - $card->save(); - - return $card; - } - - /** - * @permission administrator - * - * @param $key - * @param $active - * - * @return mixed - * - * @throws \Exception - */ - public function UpdateGenuineCardActive($key, $active) { - if (!$this->ValidateGenuineCard($key)) { - throw new InvalidInputException('Invalid card'); - } - - $card = GenuineCard::findByKey($key)[0]; - - $card->active = $active; - - $card->save(); - - return $card; - } - - /** - * @permission user - * - * @param $key - * - * @return bool - */ - public function ValidateGenuineCard($key) { - $keys = GenuineCard::findByKey($key); - - return !is_null($keys) && count($keys) == 1; - } - - private function addUserIDToFilters($userid, $filters) { - $userService = new UserService(); - $user = $userService->GetUser($userid); - - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - - if (is_null($user)) { - throw new UnauthorizedException('User not found or you do not have access'); - } - - $userFilter = Filter::Equal('userid', $user->id); - - if (is_null($filters) || $filters == '') { - $filters = $userFilter; - } else { - $filters = Filter::_And($userFilter, $filters); - } - - return $filters; - } -} diff --git a/app/services/MembershipService.php b/app/services/MembershipService.php deleted file mode 100644 index fd422d7e..00000000 --- a/app/services/MembershipService.php +++ /dev/null @@ -1,170 +0,0 @@ -Get($membershipId); - - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } - - $privs = Privilege::findByCodes(...$privArray); - - foreach ($membership->privileges->all() as $priv) { - $membership->privileges->remove($priv); - } - - foreach ($privs as $priv) { - $membership->privileges->add($priv); - } - - $membership->save(); - } - - /** - * @permission administrator - * - * @return mixed - */ - public function Update($membershipId, $title, $description, $price, $code, $days, $period) { - $membership = $this->Get($membershipId); - - $membership->title = $title; - $membership->description = $description; - $membership->price = $price; - $membership->code = $code; - $membership->days = $days; - $membership->period = $period; - - $membership->save(); - - return $membership; - } - - /** - * @permission administrator - * - * @return mixed - */ - public function UpdateActive($membershipId, $active) { - $membership = $this->Get($membershipId); - - $membership->active = $active; - - $membership->save(); - } - - /** - * @permission administrator - * - * @return mixed - */ - public function UpdatePrivate($membershipId, $private) { - $membership = $this->Get($membershipId); - - $membership->private = $private; - - $membership->save(); - } - - /** - * @permission administrator - * - * @return mixed - */ - public function UpdateRecurring($membershipId, $recurring) { - $membership = $this->Get($membershipId); - - $membership->recurring = $recurring; - - $membership->save(); - } - - /** - * @permission administrator - * - * @return mixed - */ - public function UpdateTrial($membershipId, $trial) { - $membership = $this->Get($membershipId); - - $membership->trial = $trial; - - $membership->save(); - } -} diff --git a/app/services/PaymentService.php b/app/services/PaymentService.php deleted file mode 100644 index 9e4424a1..00000000 --- a/app/services/PaymentService.php +++ /dev/null @@ -1,144 +0,0 @@ -AddUserIDOrEMailToFilters($userid, $filters); - - return Payment::count($filters); - } - - /** - * @permission administrator|user - * - * @param $id - * - * @return mixed - */ - public function GetPayment($id) { - $payment = Payment::find($id); - - if (is_null($payment)) { - return null; - } - - if (CurrentUser::getIdentity() == $payment->user_id || CurrentUser::hasAnyPermissions('administrator')) { - return $payment; - } - - return null; - } - - /** - * @permission administrator - * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * - * @return mixed - */ - public function ListPayments($page, $size, $columns, $order, $filters) { - return Payment::page($page, $size, $columns, $order, $filters); - } - - /** - * @permission administrator|user - * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * - * @return mixed - */ - public function ListUserPayments($userid, $page, $size, $columns, $order, $filters) { - $filters = $this->AddUserIDOrEMailToFilters($userid, $filters); - - return Payment::page($page, $size, $columns, $order, $filters); - } - - /** - * @permission administrator - * - * @param $paymentid - * - * @return mixed - */ - public function ReplayPaymentProcessing($paymentid) { - $log = new StringLogger(); - - $log->log('Attempting a reply of payment id: ' . $paymentid); - - $processor = new PaymentProcessor($log); - - try { - $processor->paymentCreated($paymentid); - } catch (\Exception $ex) { - $log->log('Exception: ' . $ex->getMessage()); - $log->log($ex->getTraceAsString()); - } - - $log->log('Replay complete.'); - - return $log->fullText(); - } - - private function AddUserIDOrEMailToFilters($userid, $filters) { - $userService = new UserService(); - $user = $userService->GetUser($userid); - - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - - if (is_null($user)) { - throw new UnauthorizedException('User not found or you do not have access'); - } - - $userFilter = Filter::_Or(Filter::Equal('user_id', $user->id), Filter::Equal('payer_email', $user->email)); - - if (is_null($filters) || $filters == '') { - $filters = $userFilter; - } else { - $filters = Filter::_And($userFilter, $filters); - } - - return $filters; - } -} diff --git a/app/services/PinService.php b/app/services/PinService.php deleted file mode 100644 index 80c02989..00000000 --- a/app/services/PinService.php +++ /dev/null @@ -1,190 +0,0 @@ -GetUserPin($userid); - - if (is_null($pin)) { - $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid))); - - $key = new Key(); - $key->userid = $userid; - $key->type = 'pin'; - $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); - $key->notes = 'User generated PIN'; - - $pin = $key; - - $priv = Privilege::findByCode('inherit'); - if (!is_null($priv)) { - $pin->privileges->add($priv); - } - } - - $pinid = explode('|', $pin->key)[0]; - - $pin->key = sprintf('%04s', $pinid) . '|' . sprintf('%04s', rand(0, 9999)); - $pin->notes = 'User generated PIN'; - - $pin->save(); - - return $pin; - } - - /** - * @permission gen-temp-pin|administrator - * - * @param $expires - * @param $privileges - * @param $notes - * - * @return mixed - */ - public function GenerateTemporaryPin($expires, $privileges, $notes) { - $userid = CurrentUser::getIdentity(); - - $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid))); - - $pin = new Key(); - $pin->userid = $userid; - $pin->expires = $expires; - $pin->type = 'pin'; - $pin->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); - $pin->notes = $notes; - - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } - - $privs = Privilege::findByCodes(...$privArray); - - if (!is_null($privs) && is_array($privs)) { - foreach ($privs as $priv) { - if (CurrentUser::hasAllPermissions($priv->code)) { - $pin->privileges->add($priv); - } - } - } - - $pin->save(); - - return $pin; - } - - /** - * @permission administrator|user - * - * @param $userid - * - * @return mixed - */ - public function GetUserPin($userid) { - if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { - throw new UnauthorizedException(); - } - - $keys = Key::where(Where::_And(Where::Equal(Key::Schema()->Columns()->type, 'pin'), Where::Equal(Key::Schema()->Columns()->userid, $userid))); - - if (count($keys) >= 1) { - return $keys[0]; - } - - return null; - } - - /** - * @permission administrator|user - * - * @param $keyid - * @param $pin - * - * @return mixed - */ - public function UpdatePin($keyid, $pin) { - $key = Key::find($keyid); - - if (!CurrentUser::hasAnyPermissions('administrator') && $key->userid != CurrentUser::getIdentity()) { - throw new UnauthorizedException(); - } - - $pinid = explode('|', $key->key)[0]; - - $key->key = $pinid . '|' . sprintf('%04s', intval($pin)); - - $key->save(); - } - - /** - * Change a pin. - * - * @permission administrator|user - * - * @param $pin - * - * @return mixed - */ - public function UpdateUserPin($userid, $pin) { - if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { - throw new UnauthorizedException(); - } - - $pinObj = $this->GetUserPin($userid); - - if (is_null($pin)) { - $pinObj = $this->GeneratePin($userid); - } - - $pinid = explode('|', $pinObj->key)[0]; - - $pinObj->key = $pinid . '|' . $pin; - - $pinObj->save(); - - return $pinObj; - } -} diff --git a/app/services/StripeEventService.php b/app/services/StripeEventService.php deleted file mode 100644 index d3a57ae8..00000000 --- a/app/services/StripeEventService.php +++ /dev/null @@ -1,60 +0,0 @@ -AddUserIDToFilters($userid, $filters); - - return WebHook::count($filters); - } - - /** - * @permission user - * - * @param $name - * @param $description - * @param $enabled - * @param $url - * @param $translation - * @param $headers - * @param $method - * @param $eventid - * - * @return mixed - * - * @throws UnauthorizedException - */ - public function CreateHook($name, $description, $enabled, $url, $translation, $headers, $method, $eventid) { - $event = (new EventService($this->context))->GetEvent($eventid); - - $codes = []; - foreach ($event->privileges->all() as $priv) { - array_push($codes, $priv->code); - } - - if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions($codes))) { - throw new UnauthorizedException('Insufficient privileges to subscribe to event'); - } - - $hook = new WebHook(); - - $hook->name = $name; - $hook->description = $description; - $hook->enabled = $enabled; - $hook->url = $url; - $hook->translation = $translation; - $hook->headers = $headers; - $hook->method = $method; - $hook->event = $event; - $hook->userid = CurrentUser::getIdentity(); - - return $hook->save(); - } - - /** - * @permission administrator|user - * - * @param $id - * - * @return mixed - */ - public function DeleteHook($id) { - $hook = $this->GetHook($id); - - if (is_null($hook)) { - return; - } - - $hook->delete(); - } - - /** - * @permission administrator|user - * - * @param $id - * @param $enabled - * - * @return mixed - */ - public function EnableHook($id, $enabled) { - $hook = $this->GetHook($id); - - if (is_null($hook)) { - return; - } - - $hook->enabled = $enabled; - - $hook->save(); - } - - /** - * @permission webhook|administrator - * - * @return mixed - */ - public function GetAllHooks() { - return WebHook::findAll(); - } - - /** - * @permission user|administrator - * - * @param $id - * - * @return mixed - */ - public function GetHook($id) { - $hook = WebHook::find($id); - - if (is_null($hook)) { - return null; - } - - if (CurrentUser::getIdentity() == $hook->userid || CurrentUser::hasAnyPermissions('administrator')) { - return $hook; - } - - return null; - } - - /** - * @permission webhook|administrator - * - * @param $domain - * @param $event - * - * @return mixed - */ - public function GetHooks($domain, $event) { - return WebHook::findByDomainEvent($domain, $event); - } - - /** - * @permission administrator|webhook - * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * - * @return mixed - */ - public function ListHooks($page, $size, $columns, $order, $filters) { - return WebHook::page($page, $size, $columns, $order, $filters); - } - - /** - * @permission administrator|user - * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters - * - * @return mixed - * - * @throws \Exception - */ - public function ListUserHooks($userid, $page, $size, $columns, $order, $filters) { - $filters = $this->AddUserIDToFilters($userid, $filters); - - $cols = explode(',', $columns); - - array_push($cols, 'userid'); - - $columns = implode(',', array_unique($cols)); - - return WebHook::page($page, $size, $columns, $order, $filters); - } - - /** - * @permission administrator|user - * - * @param $id - * @param $privileges - * - * @return mixed - */ - public function PutHookPrivileges($id, $privileges) { - $hook = $this->GetHook($id); - - if (is_null($hook)) { - return; - } - - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } - - $privs = Privilege::findByCodes(...$privArray); - - foreach ($hook->privileges->all() as $priv) { - $hook->privileges->remove($priv); - } - - foreach ($privs as $priv) { - if (CurrentUser::hasAnyPermissions('administrator') || CurrentUser::hasAnyPermissions($priv->code)) { - $hook->privileges->add($priv); - } - } - - $hook->save(); - } - - /** - * @permission administrator|user - * - * @param $id - * @param $name - * @param $description - * @param $enabled - * @param $url - * @param $translation - * @param $headers - * @param $method - * @param $eventid - * - * @return mixed - */ - public function UpdateHook($id, $name, $description, $enabled, $url, $translation, $headers, $method, $eventid) { - $hook = $this->GetHook($id); - - if (is_null($hook)) { - return; - } - - $event = (new EventService($this->context))->GetEvent($eventid); - - $hook->name = $name; - $hook->description = $description; - $hook->enabled = $enabled; - $hook->url = $url; - $hook->translation = $translation; - $hook->headers = $headers; - $hook->method = $method; - $hook->event = $event; - - $hook->save(); - } - - private function AddUserIDToFilters($userid, $filters) { - $userService = new UserService(); - $user = $userService->GetUser($userid); - - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - - if (is_null($user)) { - throw new UnauthorizedException('User not found or you do not have access'); - } - - $userFilter = Filter::Equal('userid', $user->id); - - if (is_null($filters) || $filters == '') { - $filters = $userFilter; - } else { - $filters = Filter::_And($userFilter, $filters); - } - - return $filters; - } -} diff --git a/circle.yml b/circle.yml deleted file mode 100644 index b5219864..00000000 --- a/circle.yml +++ /dev/null @@ -1,9 +0,0 @@ -machine: - php: - version: 8.3 -# apparently 7.0.0RC7 causes seg faults when we try to generate coverage.. disabling until circleCI supports php7+ -#test: -# pre: -# - sed -i 's/^;//' ~/.phpenv/versions/$(phpenv global)/etc/conf.d/xdebug.ini -# override: -# - ./vendor/phpunit/phpunit/phpunit --coverage-html $CIRCLE_ARTIFACTS diff --git a/composer.json b/composer.json deleted file mode 100644 index 6c3ec08e..00000000 --- a/composer.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "require": { - "aws/aws-sdk-php": "3.336.8", - "nicmart/string-template": "0.1.3", - "league/oauth2-client": "2.8.0", - "php-amqplib/php-amqplib": "2.5.2", - "stripe/stripe-php": "7.128.0", - "league/oauth2-github": "3.1.1", - "league/oauth2-google": "4.0.1" - }, - "require-dev": { - "phpunit/phpunit": "11.5.3", - "friendsofphp/php-cs-fixer": "v3.68.0" - } -} diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 140b2b87..00000000 --- a/composer.lock +++ /dev/null @@ -1,5263 +0,0 @@ -{ - "_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": "bdb5c348f93facc5eb35d2cb20f96335", - "packages": [ - { - "name": "aws/aws-crt-php", - "version": "v1.2.7", - "source": { - "type": "git", - "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", - "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", - "yoast/phpunit-polyfills": "^1.0" - }, - "suggest": { - "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "AWS SDK Common Runtime Team", - "email": "aws-sdk-common-runtime@amazon.com" - } - ], - "description": "AWS Common Runtime for PHP", - "homepage": "https://github.com/awslabs/aws-crt-php", - "keywords": [ - "amazon", - "aws", - "crt", - "sdk" - ], - "support": { - "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" - }, - "time": "2024-10-18T22:15:13+00:00" - }, - { - "name": "aws/aws-sdk-php", - "version": "3.336.8", - "source": { - "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6", - "reference": "933da0d1b9b1ac9b37d5e32e127d4581b1aabaf6", - "shasum": "" - }, - "require": { - "aws/aws-crt-php": "^1.2.3", - "ext-json": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", - "mtdowling/jmespath.php": "^2.6", - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0" - }, - "require-dev": { - "andrewsville/php-token-reflection": "^1.4", - "aws/aws-php-sns-message-validator": "~1.0", - "behat/behat": "~3.0", - "composer/composer": "^1.10.22", - "dms/phpunit-arraysubset-asserts": "^0.4.0", - "doctrine/cache": "~1.4", - "ext-dom": "*", - "ext-openssl": "*", - "ext-pcntl": "*", - "ext-sockets": "*", - "nette/neon": "^2.3", - "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", - "sebastian/comparator": "^1.2.3 || ^4.0", - "yoast/phpunit-polyfills": "^1.0" - }, - "suggest": { - "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", - "doctrine/cache": "To use the DoctrineCacheAdapter", - "ext-curl": "To send requests using cURL", - "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", - "ext-sockets": "To use client-side monitoring" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Aws\\": "src/" - }, - "exclude-from-classmap": [ - "src/data/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" - } - ], - "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", - "homepage": "http://aws.amazon.com/sdkforphp", - "keywords": [ - "amazon", - "aws", - "cloud", - "dynamodb", - "ec2", - "glacier", - "s3", - "sdk" - ], - "support": { - "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", - "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.336.8" - }, - "time": "2025-01-03T19:06:11+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "7.9.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", - "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "provide": { - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", - "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", - "psr/log": "^1.1 || ^2.0 || ^3.0" - }, - "suggest": { - "ext-curl": "Required for CURL handler support", - "ext-intl": "Required for Internationalized Domain Name (IDN) support", - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "psr-18", - "psr-7", - "rest", - "web service" - ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", - "type": "tidelift" - } - ], - "time": "2024-07-24T11:22:20+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", - "type": "tidelift" - } - ], - "time": "2024-10-17T10:06:22+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "2.7.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", - "type": "tidelift" - } - ], - "time": "2024-07-18T11:15:46+00:00" - }, - { - "name": "league/oauth2-client", - "version": "2.8.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/oauth2-client.git", - "reference": "3d5cf8d0543731dfb725ab30e4d7289891991e13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/3d5cf8d0543731dfb725ab30e4d7289891991e13", - "reference": "3d5cf8d0543731dfb725ab30e4d7289891991e13", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "php": "^7.1 || >=8.0.0 <8.5.0" - }, - "require-dev": { - "mockery/mockery": "^1.3.5", - "php-parallel-lint/php-parallel-lint": "^1.4", - "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", - "squizlabs/php_codesniffer": "^3.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\OAuth2\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alex Bilbie", - "email": "hello@alexbilbie.com", - "homepage": "http://www.alexbilbie.com", - "role": "Developer" - }, - { - "name": "Woody Gilk", - "homepage": "https://github.com/shadowhand", - "role": "Contributor" - } - ], - "description": "OAuth 2.0 Client Library", - "keywords": [ - "Authentication", - "SSO", - "authorization", - "identity", - "idp", - "oauth", - "oauth2", - "single sign on" - ], - "support": { - "issues": "https://github.com/thephpleague/oauth2-client/issues", - "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.0" - }, - "time": "2024-12-11T05:05:52+00:00" - }, - { - "name": "league/oauth2-github", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/oauth2-github.git", - "reference": "84211f62b757f7266fe605a0aa874a32f52c24fd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-github/zipball/84211f62b757f7266fe605a0aa874a32f52c24fd", - "reference": "84211f62b757f7266fe605a0aa874a32f52c24fd", - "shasum": "" - }, - "require": { - "ext-json": "*", - "league/oauth2-client": "^2.0", - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "mockery/mockery": "^1.4", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\OAuth2\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Steven Maguire", - "email": "stevenmaguire@gmail.com", - "homepage": "https://github.com/stevenmaguire" - }, - { - "name": "Woody Gilk", - "email": "woody.gilk@gmail.com", - "homepage": "https://github.com/shadowhand" - } - ], - "description": "Github OAuth 2.0 Client Provider for The PHP League OAuth2-Client", - "keywords": [ - "authorisation", - "authorization", - "client", - "github", - "oauth", - "oauth2" - ], - "support": { - "issues": "https://github.com/thephpleague/oauth2-github/issues", - "source": "https://github.com/thephpleague/oauth2-github/tree/3.1.1" - }, - "time": "2024-09-03T10:42:10+00:00" - }, - { - "name": "league/oauth2-google", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/oauth2-google.git", - "reference": "1b01ba18ba31b29e88771e3e0979e5c91d4afe76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-google/zipball/1b01ba18ba31b29e88771e3e0979e5c91d4afe76", - "reference": "1b01ba18ba31b29e88771e3e0979e5c91d4afe76", - "shasum": "" - }, - "require": { - "league/oauth2-client": "^2.0", - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "eloquent/phony-phpunit": "^6.0 || ^7.1", - "phpunit/phpunit": "^8.0 || ^9.0", - "squizlabs/php_codesniffer": "^3.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\OAuth2\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Woody Gilk", - "email": "hello@shadowhand.com", - "homepage": "https://shadowhand.com" - } - ], - "description": "Google OAuth 2.0 Client Provider for The PHP League OAuth2-Client", - "keywords": [ - "Authentication", - "authorization", - "client", - "google", - "oauth", - "oauth2" - ], - "support": { - "issues": "https://github.com/thephpleague/oauth2-google/issues", - "source": "https://github.com/thephpleague/oauth2-google/tree/4.0.1" - }, - "time": "2023-03-17T15:20:52+00:00" - }, - { - "name": "mtdowling/jmespath.php", - "version": "2.8.0", - "source": { - "type": "git", - "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", - "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "symfony/polyfill-mbstring": "^1.17" - }, - "require-dev": { - "composer/xdebug-handler": "^3.0.3", - "phpunit/phpunit": "^8.5.33" - }, - "bin": [ - "bin/jp.php" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "files": [ - "src/JmesPath.php" - ], - "psr-4": { - "JmesPath\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Declaratively specify how to extract elements from a JSON document", - "keywords": [ - "json", - "jsonpath" - ], - "support": { - "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" - }, - "time": "2024-09-04T18:46:31+00:00" - }, - { - "name": "nicmart/string-template", - "version": "v0.1.3", - "source": { - "type": "git", - "url": "https://github.com/nicmart/StringTemplate.git", - "reference": "2a62c240a35a4a20b1be8bd5aa51d4efe93ee4ae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nicmart/StringTemplate/zipball/2a62c240a35a4a20b1be8bd5aa51d4efe93ee4ae", - "reference": "2a62c240a35a4a20b1be8bd5aa51d4efe93ee4ae", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "autoload": { - "psr-4": { - "StringTemplate\\": "src/StringTemplate/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolò Martini", - "email": "nicmartnic@gmail.com" - } - ], - "description": "StringTemplate is a very simple string template engine for php. I've written it to have a thing like sprintf, but with named and nested substutions.", - "support": { - "issues": "https://github.com/nicmart/StringTemplate/issues", - "source": "https://github.com/nicmart/StringTemplate/tree/v0.1.3" - }, - "time": "2022-10-25T08:03:55+00:00" - }, - { - "name": "php-amqplib/php-amqplib", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "eb8f94d97c8e79900accf77343dbd7eca7f58506" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/eb8f94d97c8e79900accf77343dbd7eca7f58506", - "reference": "eb8f94d97c8e79900accf77343dbd7eca7f58506", - "shasum": "" - }, - "require": { - "ext-bcmath": "*", - "ext-mbstring": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "suggest": { - "ext-sockets": "Use AMQPSocketConnection" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - } - }, - "autoload": { - "psr-4": { - "PhpAmqpLib\\": "PhpAmqpLib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1" - ], - "authors": [ - { - "name": "Alvaro Videla" - } - ], - "description": "This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", - "homepage": "https://github.com/videlalvaro/php-amqplib/", - "keywords": [ - "message", - "queue", - "rabbitmq" - ], - "support": { - "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v2.5.2" - }, - "time": "2015-08-11T12:30:09+00:00" - }, - { - "name": "psr/http-client", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client" - }, - "time": "2023-09-23T14:17:50+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "stripe/stripe-php", - "version": "v7.128.0", - "source": { - "type": "git", - "url": "https://github.com/stripe/stripe-php.git", - "reference": "c704949c49b72985c76cc61063aa26fefbd2724e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/c704949c49b72985c76cc61063aa26fefbd2724e", - "reference": "c704949c49b72985c76cc61063aa26fefbd2724e", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "php": ">=5.6.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "3.5.0", - "phpstan/phpstan": "^1.2", - "phpunit/phpunit": "^5.7 || ^9.0", - "squizlabs/php_codesniffer": "^3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Stripe\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Stripe and contributors", - "homepage": "https://github.com/stripe/stripe-php/contributors" - } - ], - "description": "Stripe PHP Library", - "homepage": "https://stripe.com/", - "keywords": [ - "api", - "payment processing", - "stripe" - ], - "support": { - "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v7.128.0" - }, - "time": "2022-05-05T17:18:02+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.5-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.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", - "shasum": "" - }, - "require": { - "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.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - } - ], - "packages-dev": [ - { - "name": "clue/ndjson-react", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/stream": "^1.2" - }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Clue\\React\\NDJson\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - } - ], - "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", - "homepage": "https://github.com/clue/reactphp-ndjson", - "keywords": [ - "NDJSON", - "json", - "jsonlines", - "newline", - "reactphp", - "streaming" - ], - "support": { - "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" - }, - "funding": [ - { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2022-12-23T10:58:28+00:00" - }, - { - "name": "composer/pcre", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" - }, - "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-11-12T16:29:46+00:00" - }, - { - "name": "composer/semver", - "version": "3.4.3", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.11", - "symfony/phpunit-bridge": "^3 || ^7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-09-19T14:15:21+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", - "shasum": "" - }, - "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" - }, - "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Composer\\XdebugHandler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", - "keywords": [ - "Xdebug", - "performance" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-05-06T16:37:16+00:00" - }, - { - "name": "evenement/evenement", - "version": "v3.0.2", - "source": { - "type": "git", - "url": "https://github.com/igorw/evenement.git", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^9 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Evenement\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - } - ], - "description": "Événement is a very simple event dispatching library for PHP", - "keywords": [ - "event-dispatcher", - "event-emitter" - ], - "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/v3.0.2" - }, - "time": "2023-08-08T05:53:35+00:00" - }, - { - "name": "fidry/cpu-core-counter", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^8.5.31 || ^9.5.26", - "webmozarts/strict-phpunit": "^7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" - } - ], - "description": "Tiny utility to get the number of CPU cores.", - "keywords": [ - "CPU", - "core" - ], - "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" - }, - "funding": [ - { - "url": "https://github.com/theofidry", - "type": "github" - } - ], - "time": "2024-08-06T10:04:20+00:00" - }, - { - "name": "friendsofphp/php-cs-fixer", - "version": "v3.68.0", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", - "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", - "shasum": "" - }, - "require": { - "clue/ndjson-react": "^1.0", - "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", - "ext-filter": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.2", - "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", - "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.1 || ^6.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", - "symfony/finder": "^5.4 || ^6.4 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", - "symfony/polyfill-mbstring": "^1.31", - "symfony/polyfill-php80": "^1.31", - "symfony/polyfill-php81": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.2", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" - }, - "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.4", - "infection/infection": "^0.29.8", - "justinrainbow/json-schema": "^5.3 || ^6.0", - "keradus/cli-executor": "^2.1", - "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.7", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", - "phpunit/phpunit": "^9.6.22 || ^10.5.40 || ^11.5.2", - "symfony/var-dumper": "^5.4.48 || ^6.4.15 || ^7.2.0", - "symfony/yaml": "^5.4.45 || ^6.4.13 || ^7.2.0" - }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters." - }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "autoload": { - "psr-4": { - "PhpCsFixer\\": "src/" - }, - "exclude-from-classmap": [ - "src/Fixer/Internal/*" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" - } - ], - "description": "A tool to automatically fix PHP code style", - "keywords": [ - "Static code analysis", - "fixer", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.68.0" - }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2025-01-13T17:01:01+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.12.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2024-11-08T17:47:46+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v5.4.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", - "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.0-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.4.0" - }, - "time": "2024-12-30T11:07:19+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:33:53+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "11.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^5.3.1", - "php": ">=8.2", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-text-template": "^4.0.1", - "sebastian/code-unit-reverse-lookup": "^4.0.1", - "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", - "sebastian/lines-of-code": "^3.0.1", - "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" - }, - "require-dev": { - "phpunit/phpunit": "^11.5.0" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "11.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-12-11T12:34:27+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "5.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-08-27T05:02:59+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "5.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^11.0" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:07:44+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:08:43+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "7.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:09:35+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "11.5.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "30e319e578a7b5da3543073e30002bf82042f701" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30e319e578a7b5da3543073e30002bf82042f701", - "reference": "30e319e578a7b5da3543073e30002bf82042f701", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.8", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-invoker": "^5.0.1", - "phpunit/php-text-template": "^4.0.1", - "phpunit/php-timer": "^7.0.1", - "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.2", - "sebastian/comparator": "^6.3.0", - "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", - "sebastian/exporter": "^6.3.0", - "sebastian/global-state": "^7.0.2", - "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.0", - "sebastian/version": "^5.0.2", - "staabm/side-effects-detector": "^1.0.5" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "11.5-dev" - } - }, - "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.3" - }, - "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" - } - ], - "time": "2025-01-13T09:36:00+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": "react/cache", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, Promise-based cache interface for ReactPHP", - "keywords": [ - "cache", - "caching", - "promise", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.2.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2022-11-30T15:59:55+00:00" - }, - { - "name": "react/child-process", - "version": "v0.6.6", - "source": { - "type": "git", - "url": "https://github.com/reactphp/child-process.git", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/event-loop": "^1.2", - "react/stream": "^1.4" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/socket": "^1.16", - "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\ChildProcess\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Event-driven library for executing child processes with ReactPHP.", - "keywords": [ - "event-driven", - "process", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.6" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2025-01-01T16:37:48+00:00" - }, - { - "name": "react/dns", - "version": "v1.13.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.7 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3 || ^2", - "react/promise-timer": "^1.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Dns\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async DNS resolver for ReactPHP", - "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-06-13T14:18:03+00:00" - }, - { - "name": "react/event-loop", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "suggest": { - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], - "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-11-13T13:48:05+00:00" - }, - { - "name": "react/promise", - "version": "v3.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-05-24T10:39:05+00:00" - }, - { - "name": "react/socket", - "version": "v1.16.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.13", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.6 || ^1.2.1", - "react/stream": "^1.4" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3.3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], - "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-07-26T10:38:09+00:00" - }, - { - "name": "react/stream", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" - }, - "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], - "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.4.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-06-11T12:45:25+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:41:36+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-12-12T09:59:06+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:45:54+00:00" - }, - { - "name": "sebastian/comparator", - "version": "6.3.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/diff": "^6.0", - "sebastian/exporter": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.4" - }, - "suggest": { - "ext-bcmath": "For comparing BcMath\\Number objects" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2025-01-06T10:28:19+00:00" - }, - { - "name": "sebastian/complexity", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:49:50+00:00" - }, - { - "name": "sebastian/diff", - "version": "6.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:53:05+00:00" - }, - { - "name": "sebastian/environment", - "version": "7.2.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:54:44+00:00" - }, - { - "name": "sebastian/exporter", - "version": "6.3.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/recursion-context": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-12-05T09:17:50+00:00" - }, - { - "name": "sebastian/global-state", - "version": "7.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:57:36+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:58:38+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "6.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:00:13+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:01:32+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "6.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:10:34+00:00" - }, - { - "name": "sebastian/type", - "version": "5.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-09-17T13:12:04+00:00" - }, - { - "name": "sebastian/version", - "version": "5.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-10-09T05:16:32+00:00" - }, - { - "name": "staabm/side-effects-detector", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/staabm/side-effects-detector.git", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.6", - "phpunit/phpunit": "^9.6.21", - "symfony/var-dumper": "^5.4.43", - "tomasvotruba/type-coverage": "1.0.0", - "tomasvotruba/unused-public": "1.0.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "lib/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A static analysis tool to detect side effects in PHP code", - "keywords": [ - "static analysis" - ], - "support": { - "issues": "https://github.com/staabm/side-effects-detector/issues", - "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" - }, - "funding": [ - { - "url": "https://github.com/staabm", - "type": "github" - } - ], - "time": "2024-10-20T05:08:20+00:00" - }, - { - "name": "symfony/console", - "version": "v7.2.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.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", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.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.2.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-11T03:49:26+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", - "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", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.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.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:21:43+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", - "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.5-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.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" - }, - "require-dev": { - "symfony/process": "^6.4|^7.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.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-10-25T15:15:23+00:00" - }, - { - "name": "symfony/finder", - "version": "v7.2.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "symfony/filesystem": "^6.4|^7.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.2.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-30T19:00:17+00:00" - }, - { - "name": "symfony/options-resolver", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", - "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.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-20T11:17:29+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "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.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.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.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "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\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "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\\Php81\\": "" - }, - "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.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/process", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "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.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-06T14:24:19+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "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.5-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.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, - { - "name": "symfony/stopwatch", - "version": "v7.2.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e46690d5b9d7164a6d061cab1e8d46141b9f49df", - "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df", - "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.2.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-18T14:28:33+00:00" - }, - { - "name": "symfony/string", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" - }, - "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.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.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-13T13:31:26+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:36:25+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.6.0" -} diff --git a/conf/config.ini.php.template b/conf/config.ini.php.template deleted file mode 100644 index 1380b538..00000000 --- a/conf/config.ini.php.template +++ /dev/null @@ -1,55 +0,0 @@ - array( - "item_name" => "VHS Keyholder Membership", - "item_number" => "vhs_membership_keyholder" - ) - )); - - /** - * Show MySql Errors. - * Not recomended for live site. true/false - */ - define('DEBUG', true); diff --git a/conf/mimes.types b/conf/mimes.types deleted file mode 100644 index 7cd68a57..00000000 --- a/conf/mimes.types +++ /dev/null @@ -1,89 +0,0 @@ - -types { - text/html html htm shtml; - text/css css; - text/xml xml; - image/gif gif; - image/jpeg jpeg jpg; - application/javascript js; - application/atom+xml atom; - application/rss+xml rss; - - text/mathml mml; - text/plain txt; - text/vnd.sun.j2me.app-descriptor jad; - text/vnd.wap.wml wml; - text/x-component htc; - - image/png png; - image/tiff tif tiff; - image/vnd.wap.wbmp wbmp; - image/x-icon ico; - image/x-jng jng; - image/x-ms-bmp bmp; - image/svg+xml svg svgz; - image/webp webp; - - application/font-woff woff; - application/java-archive jar war ear; - application/json json; - application/mac-binhex40 hqx; - application/msword doc; - application/pdf pdf; - application/postscript ps eps ai; - application/rtf rtf; - application/vnd.apple.mpegurl m3u8; - application/vnd.ms-excel xls; - application/vnd.ms-fontobject eot; - application/vnd.ms-powerpoint ppt; - application/vnd.wap.wmlc wmlc; - application/vnd.google-earth.kml+xml kml; - application/vnd.google-earth.kmz kmz; - application/x-7z-compressed 7z; - application/x-cocoa cco; - application/x-java-archive-diff jardiff; - application/x-java-jnlp-file jnlp; - application/x-makeself run; - application/x-perl pl pm; - application/x-pilot prc pdb; - application/x-rar-compressed rar; - application/x-redhat-package-manager rpm; - application/x-sea sea; - application/x-shockwave-flash swf; - application/x-stuffit sit; - application/x-tcl tcl tk; - application/x-x509-ca-cert der pem crt; - application/x-xpinstall xpi; - application/xhtml+xml xhtml; - application/xspf+xml xspf; - application/zip zip; - - application/octet-stream bin exe dll; - application/octet-stream deb; - application/octet-stream dmg; - application/octet-stream iso img; - application/octet-stream msi msp msm; - - application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; - application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; - - audio/midi mid midi kar; - audio/mpeg mp3; - audio/ogg ogg; - audio/x-m4a m4a; - audio/x-realaudio ra; - - video/3gpp 3gpp 3gp; - video/mp2t ts; - video/mp4 mp4; - video/mpeg mpeg mpg; - video/quicktime mov; - video/webm webm; - video/x-flv flv; - video/x-m4v m4v; - video/x-mng mng; - video/x-ms-asf asx asf; - video/x-ms-wmv wmv; - video/x-msvideo avi; -} diff --git a/conf/nginx-vhost-docker-compose.conf b/conf/nginx-vhost-docker-compose.conf deleted file mode 100644 index 23453705..00000000 --- a/conf/nginx-vhost-docker-compose.conf +++ /dev/null @@ -1,30 +0,0 @@ -server { - listen 80 default_server; - server_name _; - - location ~ (/services/|\.php$) { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass nomos-backend:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /var/www/html/app/app.php; # ?service=$fastcgi_script_name; - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; - include fastcgi_params; - } - - - location / { - root /var/www/html; - } - - - location = /robots.txt { - return 200 "User-agent: *\nDisallow: /"; - } - - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass nomos-backend:9000; - } -} diff --git a/conf/nginx-vhost-docker.conf b/conf/nginx-vhost-docker.conf deleted file mode 100644 index 70ef1db4..00000000 --- a/conf/nginx-vhost-docker.conf +++ /dev/null @@ -1,32 +0,0 @@ -server { - set $app_root "/www"; - - listen 80 default_server; - server_name _; - - location ~ (/services/|\.php$) { - include fastcgi_params; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root/app/app.php; # ?service=$fastcgi_script_name; - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; - } - - - location / { - root $app_root/web; - } - - - location = /robots.txt { - return 200 "User-agent: *\nDisallow: /"; - } - - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - } -} diff --git a/conf/nginx-vhost-vagrant.conf b/conf/nginx-vhost-vagrant.conf deleted file mode 100644 index 40b7b988..00000000 --- a/conf/nginx-vhost-vagrant.conf +++ /dev/null @@ -1,34 +0,0 @@ -server { - set $app_root "/vagrant"; - - listen 80 ; - server_name _ - membership.vanhack.ca - 192.168.38.10; - - location ~ (/services/|\.php$) { - include fastcgi_params; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root/app/app.php; # ?service=$fastcgi_script_name; - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; - } - - - location / { - root $app_root/web; - } - - - location = /robots.txt { - return 200 "User-agent: *\nDisallow: /"; - } - - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - } -} \ No newline at end of file diff --git a/conf/nginx-vhost-windows.conf b/conf/nginx-vhost-windows.conf deleted file mode 100644 index 59b4ca2f..00000000 --- a/conf/nginx-vhost-windows.conf +++ /dev/null @@ -1,56 +0,0 @@ -server { - - set $app_root "D:/Dropbox/Source/VHS/membership-manager-pro"; - - listen 80; - server_name membership.dev.vanhack.ca; - - #for development only - location ~ (/tools/.+\.php)$ { - fastcgi_intercept_errors on; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root$fastcgi_script_name; - include fastcgi_params; - } - - location ~ (/services/|\.php$) { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_read_timeout 300; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $app_root/app/app.php; # ?service=$fastcgi_script_name; - include fastcgi_params; - } - - location / { - root $app_root/web; - } - - location ~* \.php$ { - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - } - - location ~ (/assets/) { - root $app_root/scripts; - } - - location ~ (/theme/) { - root $app_root/scripts; - } - - location ~ (/cache/) { - root $app_root/cache; - } - - location ~ (/uploads/) { - root $app_root/uploads; - } - - location ~ (/badges/) { - root $app_root/badges; - } -} diff --git a/diff-report.txt b/diff-report.txt new file mode 100644 index 00000000..4c84c928 --- /dev/null +++ b/diff-report.txt @@ -0,0 +1,1845 @@ +============================================================ +========== app/app.php +============================================================ +-if (DEBUG) { ++if (defined('DEBUG')) { +-$serverLog = new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/server.log'); ++$serverLog = new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/server.log'); ++\vhs\web\HttpContext::Server()->register(new \vhs\web\modules\HttpJsonServiceHandlerModule('v2')); ++\vhs\gateways\Engine::getInstance()->setLogger($serverLog); +============================================================ +========== app/contracts/IAuthService1.php +============================================================ ++use app\domain\AppClient; +============================================================ +========== app/domain/EmailTemplate.php +============================================================ ++use app\dto\GeneratedEmailResults; +- $ret = []; ++ $ret = new GeneratedEmailResults(); +- $ret['subject'] = $engine->render($template->subject, $context); +- $ret['txt'] = $engine->render($template->body, $context); +- $ret['html'] = $engine->render($template->html, $context); ++ $ret->subject = $engine->render($template->subject, $context); ++ $ret->txt = $engine->render($template->body, $context); ++ $ret->html = $engine->render($template->html, $context); +============================================================ +========== app/domain/Event.php +============================================================ +- Where::_And(Where::Equal(Event::Schema()->Columns()->domain, $domain), Where::Equal(Event::Schema()->Columns()->event, $event)) ++ Where::_And( ++ Where::Equal(Event::Schema()->Columns()->domain, $domain), ++ Where::Equal(Event::Schema()->Columns()->event, $event) ++ ) +============================================================ +========== app/domain/Key.php +============================================================ +- return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, 'api'), Where::Equal(KeySchema::Columns()->key, $key))); ++ return self::where( ++ Where::_And( ++ Where::Equal(KeySchema::Columns()->type, 'api'), ++ Where::Equal(KeySchema::Columns()->key, $key) ++ ) ++ ); +- return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, 'pin'), Where::Equal(KeySchema::Columns()->key, $pin))); ++ return self::where( ++ Where::_And( ++ Where::Equal(KeySchema::Columns()->type, 'pin'), ++ Where::Equal(KeySchema::Columns()->key, $pin) ++ ) ++ ); +- return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, 'rfid'), Where::Equal(KeySchema::Columns()->key, $rfid))); ++ return self::where( ++ Where::_And( ++ Where::Equal(KeySchema::Columns()->type, 'rfid'), ++ Where::Equal(KeySchema::Columns()->key, $rfid) ++ ) ++ ); +- return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, $service), Where::Equal(KeySchema::Columns()->key, $key))); ++ return self::where( ++ Where::_And( ++ Where::Equal(KeySchema::Columns()->type, $service), ++ Where::Equal(KeySchema::Columns()->key, $key) ++ ) ++ ); +- return self::where(Where::_And(Where::Equal(KeySchema::Columns()->type, $type), Where::Equal(KeySchema::Columns()->key, $key))); ++ return self::where( ++ Where::_And( ++ Where::Equal(KeySchema::Columns()->type, $type), ++ Where::Equal(KeySchema::Columns()->key, $key) ++ ) ++ ); +- return self::where(Where::_And(Where::Null(KeySchema::Columns()->userid), Where::Equal(KeySchema::Columns()->type, 'api'))); ++ return self::where( ++ Where::_And( ++ Where::Null(KeySchema::Columns()->userid), ++ Where::Equal(KeySchema::Columns()->type, 'api') ++ ) ++ ); +- return self::where(Where::_And(Where::Equal(Key::Schema()->Columns()->type, 'api'), Where::Equal(Key::Schema()->Columns()->userid, $userid))); ++ return self::where( ++ Where::_And( ++ Where::Equal(Key::Schema()->Columns()->type, 'api'), ++ Where::Equal(Key::Schema()->Columns()->userid, $userid) ++ ) ++ ); +============================================================ +========== app/domain/Membership.php +============================================================ +- Where::_And(Where::LesserEqual(MembershipSchema::Columns()->price, $price), Where::Equal(MembershipSchema::Columns()->active, true)), ++ Where::_And( ++ Where::LesserEqual(MembershipSchema::Columns()->price, $price), ++ Where::Equal(MembershipSchema::Columns()->active, true) ++ ), +============================================================ +========== app/domain/Payment.php +============================================================ +- Query::select(PaymentSchema::Table(), PaymentSchema::Columns(), Where::Equal(PaymentSchema::Columns()->txn_id, $txn_id)) ++ Query::select( ++ PaymentSchema::Table(), ++ PaymentSchema::Columns(), ++ Where::Equal(PaymentSchema::Columns()->txn_id, $txn_id) ++ ) +============================================================ +========== app/domain/Privilege.php +============================================================ +- public static function findByCodes(...$codes) { ++ public static function findByCodes(string ...$codes) { +- private static function checkCodeAccess(...$codes) { ++ private static function checkCodeAccess(string ...$codes) { +============================================================ +========== app/domain/SystemPreference.php +============================================================ +- public static function findByKey($key, callable $accessCheck = null) { ++ public static function findByKey($key, ?callable $accessCheck = null) { +============================================================ +========== app/domain/User.php +============================================================ ++use app\dto\UserActiveEnum; +- if ($this->active != 'y') { ++ if ($this->active != UserActiveEnum::ACTIVE->value) { +- if ($this->active != 'y') { ++ if ($this->active != UserActiveEnum::ACTIVE->value) { +- $this->validateEmail($results); +============================================================ +========== app/domain/WebHook.php +============================================================ +- Where::_And(Where::Equal(WebHook::Schema()->Columns()->domain, $domain), Where::Equal(WebHook::Schema()->Columns()->event, $event)) ++ Where::_And( ++ Where::Equal(WebHook::Schema()->Columns()->domain, $domain), ++ Where::Equal(WebHook::Schema()->Columns()->event, $event) ++ ) +============================================================ +========== app/exceptions/InvalidInputException.php +============================================================ +-class InvalidInputException extends \Exception { +- public function __construct($message = 'Invalid input') { +- parent::__construct($message); ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; ++class InvalidInputException extends HttpException { ++ public function __construct($message = 'Invalid input', $code = HttpStatusCodes::Client_Error_Bad_Request) { ++ parent::__construct($message, $code); +============================================================ +========== app/exceptions/InvalidPasswordHashException.php +============================================================ +-class InvalidPasswordHashException extends \Exception { ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; ++class InvalidPasswordHashException extends HttpException { +- parent::__construct($message); ++ parent::__construct($message, HttpStatusCodes::Client_Error_Unauthorized); +============================================================ +========== app/exceptions/MemberCardException.php +============================================================ +-class MemberCardException extends \Exception { +- public function __construct($message = 'Unexpected membercard issue') { +- parent::__construct($message); ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; ++class MemberCardException extends HttpException { ++ public function __construct($message = 'Unexpected membercard issue', $code = HttpStatusCodes::Client_Error_Im_a_teapot) { ++ parent::__construct($message, $code); +============================================================ +========== app/exceptions/UserAlreadyExistsException.php +============================================================ +-class UserAlreadyExistsException extends \Exception { ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; ++class UserAlreadyExistsException extends HttpException { +- parent::__construct($message); ++ parent::__construct($message, HttpStatusCodes::Client_Error_Failed_Dependency); +============================================================ +========== app/gateways/PaymentGatewayException.php +============================================================ +-class PaymentGatewayException extends \Exception { +-} ++class PaymentGatewayException extends \Exception {} +============================================================ +========== app/gateways/PaypalGateway.php +============================================================ +- curl_setopt($ch, CURLOPT_POST, 1); +- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); ++ curl_setopt($ch, CURLOPT_POST, true); ++ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); +- curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); ++ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); ++ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); ++ curl_setopt($ch, CURLOPT_FORBID_REUSE, true); +============================================================ +========== app/gateways/StripeGateway.php +============================================================ ++use vhs\web\enums\HttpStatusCodes; +- \Stripe\Stripe::setApiKey(STRIPE_API_KEY); ++ Stripe::setApiKey(STRIPE_API_KEY); ++ http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); +- http_response_code(400); +- exit(); +- } catch (\UnexpectedValueException $e) { +- throw new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); +- http_response_code(400); +- exit(); +- } catch (\Stripe\Exception\SignatureVerificationException $e) { +- throw new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); +- http_response_code(400); +- exit(); +- } finally { ++ } catch (\UnexpectedValueException $e) { ++ http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); ++ return new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); ++ } catch (\Stripe\Exception\SignatureVerificationException $e) { ++ http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); ++ return new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); +============================================================ +========== app/include.php +============================================================ +-$sqlLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/sql.log') : new \vhs\loggers\SilentLogger(); ++$sqlLog = DEBUG ? new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/sql.log') : new \vhs\loggers\SilentLogger(); +-$rabbitLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/rabbit.log') : new \vhs\loggers\SilentLogger(); ++$rabbitLog = DEBUG ? new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/rabbit.log') : new \vhs\loggers\SilentLogger(); +- new \vhs\messaging\engines\RabbitMQ\RabbitMQConnectionInfo(RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USER, RABBITMQ_PASSWORD, RABBITMQ_VHOST) ++ new \vhs\messaging\engines\RabbitMQ\RabbitMQConnectionInfo(RABBITMQ_HOST, (int) RABBITMQ_PORT, RABBITMQ_USER, RABBITMQ_PASSWORD, RABBITMQ_VHOST) +-$serviceLog = DEBUG ? new \vhs\loggers\FileLogger(dirname(__FILE__) . '/service.log') : new \vhs\loggers\SilentLogger(); ++$serviceLog = DEBUG ? new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/service.log') : new \vhs\loggers\SilentLogger(); +-\vhs\services\ServiceRegistry::register($serviceLog, 'web', 'app\\endpoints\\web', ROOT_NAMESPACE_PATH); ++\vhs\services\ServiceRegistry::register($serviceLog, 'v2', 'app\\endpoints\\v2', ROOT_NAMESPACE_PATH); ++\vhs\services\ServiceRegistry::register($serviceLog, 'web', 'app\\endpoints\\web', ROOT_NAMESPACE_PATH); +============================================================ +========== app/modules/HttpPaymentGatewayHandler.php +============================================================ +- public function handle(HttpServer $server) { ++ public function handle(HttpServer $server): void { +============================================================ +========== app/modules/HttpPaymentGatewayHandlerModule.php +============================================================ +- private function __clone() { +- } ++ public function __clone(): void {} +============================================================ +========== app/monitors/DomainEventMonitor.php +============================================================ +- public function Init(Logger &$logger = null) { ++ public function Init(?Logger &$logger = null) { +============================================================ +========== app/monitors/PaymentMonitor.php +============================================================ +-use app\domain\User; +-use Aws\CloudFront\Exception\Exception; +- public function Init(Logger &$logger = null) { ++ public function Init(?Logger &$logger = null) { +============================================================ +========== app/monitors/PaypalIpnMonitor.php +============================================================ +- public function Init(Logger &$logger = null) { ++ public function Init(?Logger &$logger = null) { +============================================================ +========== app/monitors/StripeEventMonitor.php +============================================================ +- $item_amount = !is_null($line_item->price->unit_amount / 100) ? $line_item->price->unit_amount / 100 : $item_amount; ++ $item_amount = !is_null($line_item->price->unit_amount) ? $line_item->price->unit_amount / 100 : $item_amount; +- public function Init(Logger &$logger = null) { ++ public function Init(?Logger &$logger = null) { +============================================================ +========== app/processors/PaymentProcessor.php +============================================================ ++use app\adapters\v2\EmailAdapter2; ++use app\dto\UserActiveEnum; +-use app\services\EmailService; +- private $emailService; +- public function __construct(Logger &$logger = null) { ++ public function __construct(?Logger &$logger = null) { +- $this->emailService = new EmailService(); +- $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ +- 'subject' => '[Nomos] Unknown user made a random donation - ' . $payment->payer_fname . ' ' . $payment->lname, ++ EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ ++ 'subject' => '[Nomos] Unknown user made a random donation - ' . $payment->payer_fname . ' ' . $payment->payer_lname, +- $payment->fname . ++ $payment->payer_fname . +- $payment->lname . ++ $payment->payer_lname . +- $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_donation_random', [ ++ EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_donation_random', [ +- $this->emailService->EmailUser($user, 'donation_random', [ ++ EmailAdapter2::getInstance()->EmailUser($user, 'donation_random', [ +- $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_newuser', [ ++ EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_newuser', [ +- if ($user->active == 'n') { ++ if ($user->active == UserActiveEnum::INACTIVE->value) { +- if ($user->active != 'y') { +- $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ +- 'subject' => "[Nomos] User made payment but isn't active - " . $payment->payer_fname . ' ' . $payment->lname, ++ if ($user->active != UserActiveEnum::ACTIVE->value) { ++ EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ ++ 'subject' => "[Nomos] User made payment but isn't active - " . $payment->payer_fname . ' ' . $payment->payer_lname, +- $payment->fname . ++ $payment->payer_fname . +- $payment->lname . ++ $payment->payer_lname . +- $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_payment', [ ++ EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_payment', [ +- $this->emailService->EmailUser($user, 'payment', [ ++ EmailAdapter2::getInstance()->EmailUser($user, 'payment', [ +- $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ +- 'subject' => '[Nomos] Unknown user purchased Membership Card - ' . $payment->payer_fname . ' ' . $payment->lname, ++ EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ ++ 'subject' => '[Nomos] Unknown user purchased Membership Card - ' . $payment->payer_fname . ' ' . $payment->payer_lname, +- $payment->fname . ++ $payment->payer_fname . +- $payment->lname . ++ $payment->payer_lname . +- $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_membercard_purchased', [ ++ EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_membercard_purchased', [ +- $this->emailService->EmailUser($user, 'membercard_purchased', [ ++ EmailAdapter2::getInstance()->EmailUser($user, 'membercard_purchased', [ +============================================================ +========== app/schema/AccessTokenSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->appclientid, AppClientSchema::Table(), AppClientSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->userid, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->appclientid, ++ AppClientSchema::Table(), ++ AppClientSchema::Columns()->id ++ ) +============================================================ +========== app/schema/AppClientSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->userid, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ) +============================================================ +========== app/schema/EventPrivilegeSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->eventid, EventSchema::Table(), EventSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->eventid, ++ EventSchema::Table(), ++ EventSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->privilegeid, ++ PrivilegeSchema::Table(), ++ PrivilegeSchema::Columns()->id ++ ) +============================================================ +========== app/schema/KeyPrivilegeSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->keyid, KeySchema::Table(), KeySchema::Columns()->id), +- Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->keyid, ++ KeySchema::Table(), ++ KeySchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->privilegeid, ++ PrivilegeSchema::Table(), ++ PrivilegeSchema::Columns()->id ++ ) +============================================================ +========== app/schema/KeySchema.php +============================================================ +- Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->userid, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ) +============================================================ +========== app/schema/MembershipPrivilegeSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->membershipid, MembershipSchema::Table(), MembershipSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->membershipid, ++ MembershipSchema::Table(), ++ MembershipSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->privilegeid, ++ PrivilegeSchema::Table(), ++ PrivilegeSchema::Columns()->id ++ ) +============================================================ +========== app/schema/PasswordResetRequestSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->userid, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ) +============================================================ +========== app/schema/PaymentSchema.php +============================================================ +-use app\schema\MembershipSchema; +-use app\schema\UserSchema; +- Constraint::ForeignKey($table->columns->membership_id, MembershipSchema::Table(), MembershipSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->user_id, UserSchema::Table(), UserSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->membership_id, ++ MembershipSchema::Table(), ++ MembershipSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->user_id, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ) +============================================================ +========== app/schema/RefreshTokenSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->appclientid, AppClientSchema::Table(), AppClientSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->userid, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->appclientid, ++ AppClientSchema::Table(), ++ AppClientSchema::Columns()->id ++ ) +============================================================ +========== app/schema/SystemPreferencePrivilegeSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->systempreferenceid, SystemPreferenceSchema::Table(), SystemPreferenceSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->systempreferenceid, ++ SystemPreferenceSchema::Table(), ++ SystemPreferenceSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->privilegeid, ++ PrivilegeSchema::Table(), ++ PrivilegeSchema::Columns()->id ++ ) +============================================================ +========== app/schema/UserPrivilegeSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->userid, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->privilegeid, ++ PrivilegeSchema::Table(), ++ PrivilegeSchema::Columns()->id ++ ) +============================================================ +========== app/schema/UserSchema.php +============================================================ ++use app\dto\UserActiveEnum; +- $table->addColumn('active', Type::Enum('n', 'y', 't', 'b')); ++ $table->addColumn( ++ 'active', ++ Type::Enum(UserActiveEnum::INACTIVE->value, UserActiveEnum::ACTIVE->value, UserActiveEnum::PENDING->value, UserActiveEnum::BANNED->value) ++ ); +- Constraint::ForeignKey($table->columns->membership_id, MembershipSchema::Table(), MembershipSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->membership_id, ++ MembershipSchema::Table(), ++ MembershipSchema::Columns()->id ++ ) +============================================================ +========== app/schema/WebHookPrivilegeSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->webhookid, WebHookSchema::Table(), WebHookSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->privilegeid, PrivilegeSchema::Table(), PrivilegeSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->webhookid, ++ WebHookSchema::Table(), ++ WebHookSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->privilegeid, ++ PrivilegeSchema::Table(), ++ PrivilegeSchema::Columns()->id ++ ) +============================================================ +========== app/schema/WebHookSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->userid, UserSchema::Table(), UserSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->eventid, EventSchema::Table(), EventSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->userid, ++ UserSchema::Table(), ++ UserSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->eventid, ++ EventSchema::Table(), ++ EventSchema::Columns()->id ++ ) +============================================================ +========== app/security/Authenticate.php +============================================================ ++use app\dto\UserActiveEnum; ++use app\exceptions\InvalidAccessTokenCredentialsException; ++use app\exceptions\InvalidKeyCredentialsException; +- throw new InvalidCredentials(message: '"Invalid access token"'); ++ throw new InvalidAccessTokenCredentialsException(); +- throw new InvalidCredentials('"Invalid access token"'); ++ throw new InvalidAccessTokenCredentialsException(); +- array_map(function ($privilege) { +- return $privilege->code; +- }, $user->privileges->all()) ++ array_map( ++ function ($privilege) { ++ return $privilege->code; ++ }, ++ $user->privileges->all() ++ ) +- if (isset($_SERVER) && array_key_exists('REMOTE_ADDR', $_SERVER)) { ++ if (array_key_exists('REMOTE_ADDR', $_SERVER)) { +- private static function isUserValid($user) { ++ private static function isUserValid(User $user) { +- case 'n': //not active ++ case UserActiveEnum::INACTIVE->value: //not active +- break; +- case 'y': //yes they are active ++ case UserActiveEnum::ACTIVE->value: //yes they are active +- break; +- case 't': //pending email verification ++ case UserActiveEnum::PENDING->value: //pending email verification +- break; +- case 'b': //banned ++ case UserActiveEnum::BANNED->value: //banned +- break; ++ default: ++ return false; +- return false; +- throw new InvalidCredentials('"Invalid key"'); ++ throw new InvalidKeyCredentialsException(); +- throw new InvalidCredentials('"Invalid key"'); ++ throw new InvalidKeyCredentialsException(); +- array_map(function ($privilege) { +- return $privilege->code; +- }, $user->privileges->all()) ++ array_map( ++ function ($privilege) { ++ return $privilege->code; ++ }, ++ $user->privileges->all() ++ ) +- throw new InvalidCredentials('"Invalid key"'); ++ throw new InvalidKeyCredentialsException(); +- CurrentUser::setPrincipal(new TokenPrincipal($identity, $privileges, $grants, $name)); ++ CurrentUser::setPrincipal(new TokenPrincipal(id: $identity, permissions: $privileges, grants: $grants, name: $name)); +============================================================ +========== app/security/ColumnPrivilegedAccess.php +============================================================ +- private $privileges; ++ private $privileges = []; +- public function serialize(): mixed { ++ public function serialize(): string { ++ return json_encode($this->__serialize()); ++ } ++ public function __serialize(): array { +- public function __serialize() { +- return $this->serialize(); +- } +============================================================ +========== app/security/PasswordUtil.php +============================================================ +- gettype($testVal) === 'string' && $testVal !== ''; ++ return gettype($testVal) === 'string' && $testVal !== ''; +============================================================ +========== app/security/PrivilegedAccess.php +============================================================ +- public function __construct(Column $ownerColumn = null) { ++ public function __construct(?Column $ownerColumn = null) { +- public static function GenerateAccess($key, Table $table, Column $ownerColumn = null) { ++ public static function GenerateAccess($key, Table $table, ?Column $ownerColumn = null) { +- return CurrentUser::hasAnyPermissions($privileges); ++ return CurrentUser::hasAnyPermissions(...$privileges); +- public function serialize(): mixed { +- return [ +- 'type' => 'ownership', +- 'ownership' => [ +- 'table' => $this->ownerColumn->table->name, +- 'name' => $this->ownerColumn->name, +- 'type' => $this->ownerColumn->type +- ], +- 'checks' => $this->checks +- ]; ++ public function serialize(): string { ++ return json_encode($this->__serialize()); +- public function __serialize() { +- return $this->serialize(); ++ public function __serialize(): array { ++ return [ ++ 'type' => 'ownership', ++ 'ownership' => [ ++ 'table' => $this->ownerColumn->table->name, ++ 'name' => $this->ownerColumn->name, ++ 'type' => $this->ownerColumn->type ++ ], ++ 'checks' => $this->checks ++ ]; +============================================================ +========== app/security/TablePrivilegedAccess.php +============================================================ +- public function serialize(): mixed { ++ public function serialize(): string { ++ return json_encode($this->__serialize()); ++ } ++ public function __serialize(): array { +- public function __serialize() { +- return $this->serialize(); +- } +============================================================ +========== app/security/credentials/TokenCredentials.php +============================================================ +- private $token; ++ private string $token; +============================================================ +========== app/security/oauth/modules/GithubOAuthHandler.php +============================================================ +- if ($_GET['action'] == 'link' && !is_null($userDetails)) { ++ if ($_GET['action'] == 'link' && $userDetails !== null) { +============================================================ +========== app/security/oauth/modules/OAuthHandlerModule.php +============================================================ +- private function __clone() { +- } ++ public function __clone(): void {} +============================================================ +========== app/security/oauth/providers/slack/Slack.php +============================================================ +-use app\security\oauth\providers\slack\SlackProviderException; +- public function getBaseAccessTokenUrl(array $params): string { ++ public function getBaseAccessTokenUrl(mixed $params): string { +- return SlackProviderException::fromResponse($response, $data['error']); ++ SlackProviderException::fromResponse($response, $data['error']); +============================================================ +========== app/security/oauth/providers/slack/SlackProviderException.php +============================================================ +-class SlackProviderException extends IdentityProviderException { ++final class SlackProviderException extends IdentityProviderException { +============================================================ +========== app/security/oauth/providers/slack/SlackUser.php +============================================================ +- $this->response['user_id']; ++ return $this->response['user_id']; +============================================================ +========== app/services/ApiKeyService.php +============================================================ +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = explode(',', $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +- foreach ($privs as $priv) { +- $key->privileges->add($priv); ++ if (!is_null($privs)) { ++ foreach ($privs as $priv) { ++ $key->privileges->add($priv); ++ } +============================================================ +========== app/services/AuthService.php +============================================================ ++use vhs\domain\Domain; ++ private static function parseValidAccount(&$key, &$user, &$retval): bool { ++ if ($user->valid) { ++ $retval['valid'] = true; ++ $retval['userId'] = $user->id; ++ $retval['username'] = $user->username; ++ $retval['type'] = $user->membership->code; ++ $retval['privileges'] = $key->getAbsolutePrivileges(); ++ return true; ++ } else { ++ $retval['username'] = $user->username; ++ $retval['message'] = $user->getInvalidReason(); ++ } ++ return false; ++ } +- if ($user->valid) { +- $retval['valid'] = true; +- $retval['userId'] = $user->id; +- $retval['username'] = $user->username; +- $retval['type'] = $user->membership->code; +- $retval['privileges'] = $key->getAbsolutePrivileges(); +- $logAccess(true, $user->id); ++ if ($user == null || !$user instanceof User) { ++ $logAccess(false); +- } else { +- $retval['username'] = $user->username; +- $retval['message'] = $user->getInvalidReason(); +- $logAccess(false, $user->id); ++ $isValid = self::parseValidAccount($key, $user, $retval); ++ $logAccess($isValid, $user->id); +- if ($user->valid) { +- $retval['valid'] = true; +- $retval['userId'] = $user->id; +- $retval['username'] = $user->username; +- $retval['type'] = $user->membership->code; +- $retval['privileges'] = $key->getAbsolutePrivileges(); +- $logAccess(true, $user->id); ++ if ($user == null || !$user instanceof User) { ++ $logAccess(false); +- } else { +- $retval['username'] = $user->username; +- $retval['message'] = $user->getInvalidReason(); +- $logAccess(false, $user->id); ++ $isValid = self::parseValidAccount($key, $user, $retval); ++ $logAccess($isValid, $user->id); +- if ($user->valid) { +- $retval['valid'] = true; +- $retval['userId'] = $user->id; +- $retval['username'] = $user->username; +- $retval['type'] = $user->membership->code; +- $retval['privileges'] = $key->getAbsolutePrivileges(); +- $logAccess(true, $user->id); ++ if ($user == null || !$user instanceof User) { ++ $logAccess(false); +- } else { +- $retval['username'] = $user->username; +- $retval['message'] = $user->getInvalidReason(); +- $logAccess(false, $user->id); ++ $isValid = self::parseValidAccount($key, $user, $retval); ++ $logAccess($isValid, $user->id); +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } ++ Domain::coerceFilters($filters); +- $expiry = new \DateTime($expires); ++ $expiry = new DateTime($expires); +- $expiry = new \DateTime($expires); ++ $expiry = new DateTime($expires); +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } ++ Domain::coerceFilters($filters); +============================================================ +========== app/services/EmailService.php +============================================================ ++use vhs\services\Service; +-class EmailService implements IEmailService1 { ++class EmailService extends Service implements IEmailService1 { +- $subject = $generated['subject']; ++ $subject = $generated->subject; +- 'Data' => $generated['txt'] ++ 'Data' => $generated->txt +- 'Data' => $generated['html'] ++ 'Data' => $generated->html +============================================================ +========== app/services/EventService.php +============================================================ +-use Aws\CloudFront\Exception\Exception; +-use vhs\domain\Domain; +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = explode(',', $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +============================================================ +========== app/services/KeyService.php +============================================================ +- $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid)); ++ $nextpinid = Database::scalar( ++ Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid) ++ ); +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = []; +- array_push($privArray, $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +============================================================ +========== app/services/MemberCardService.php +============================================================ ++use vhs\domain\Domain; ++use vhs\services\Service; +-class MemberCardService implements IMemberCardService1 { ++class MemberCardService extends Service implements IMemberCardService1 { +- Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), //TODO eventually put these into card campaigns or something ++ Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), +- Query::Select(GenuineCard::Schema()->Table(), new Columns(GenuineCard::Schema()->Columns()->paymentid)) ++ Query::Select( ++ GenuineCard::Schema()->Table(), ++ new Columns(GenuineCard::Schema()->Columns()->paymentid) ++ ) +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } ++ Domain::coerceFilters($filters); +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } ++ Domain::coerceFilters($filters); +============================================================ +========== app/services/MembershipService.php +============================================================ ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; +- return []; ++ throw new HttpException('Sorry, no dice!', HttpStatusCodes::Server_Error_Not_Implemented); +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = explode(',', $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +- foreach ($privs as $priv) { +- $membership->privileges->add($priv); ++ if (!empty($privs)) { ++ foreach ($privs as $priv) { ++ $membership->privileges->add($priv); ++ } +============================================================ +========== app/services/PaymentService.php +============================================================ +-use Aws\CloudFront\Exception\Exception; ++use vhs\domain\Domain; +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } ++ Domain::coerceFilters($filters); +============================================================ +========== app/services/PinService.php +============================================================ +- $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid))); ++ $nextpinid = Database::scalar( ++ Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid)) ++ ); +- $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid))); ++ $nextpinid = Database::scalar( ++ Query::Select( ++ SettingsSchema::Table(), ++ new Columns(SettingsSchema::Columns()->nextpinid) ++ ) ++ ); +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = explode(',', $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +- $keys = Key::where(Where::_And(Where::Equal(Key::Schema()->Columns()->type, 'pin'), Where::Equal(Key::Schema()->Columns()->userid, $userid))); ++ $keys = Key::where( ++ Where::_And( ++ Where::Equal(Key::Schema()->Columns()->type, 'pin'), ++ Where::Equal(Key::Schema()->Columns()->userid, $userid) ++ ) ++ ); +============================================================ +========== app/services/PreferenceService.php +============================================================ +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = explode(',', $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +============================================================ +========== app/services/PrivilegeService.php +============================================================ +-use app\exceptions\MemberCardException; +-use vhs\security\exceptions\UnauthorizedException; +============================================================ +========== app/services/UserService.php +============================================================ ++use app\dto\UserActiveEnum; +- $user->active = 't'; ++ $user->active = UserActiveEnum::PENDING->value; +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = explode(',', $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +- $user->active = 't'; ++ $user->active = UserActiveEnum::PENDING->value; +- curl_setopt($ch, CURLOPT_POST, 1); +- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); ++ curl_setopt($ch, CURLOPT_POST, true); ++ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); +- curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); ++ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); ++ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); ++ curl_setopt($ch, CURLOPT_FORBID_REUSE, true); +============================================================ +========== app/services/WebHookService.php +============================================================ +-use app\exceptions\MemberCardException; ++use vhs\domain\Domain; +- if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions($codes))) { ++ if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions(...$codes))) { +- $privArray = $privileges; +- if (!is_array($privArray)) { +- $privArray = explode(',', $privileges); +- } ++ $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; +- if (is_string($filters)) { +- $filters = json_decode($filters); +- } ++ Domain::coerceFilters($filters); +============================================================ +========== tests/ConstantsTest.php +============================================================ +-use app\constants\DateTime; ++use app\constants\Formats; +- public function test_DateTime_DATE_TIME_MIDNIGHT() { +- $this->assertEquals('Y-m-d 00:00:00', DateTime::DATE_TIME_MIDNIGHT); ++ public function test_DateTime_DATE_TIME_MIDNIGHT(): void { ++ $this->assertEquals('Y-m-d 00:00:00', Formats::DATE_TIME_ISO_SHORT_MIDNIGHT); +- public function test_DateTime_DATE_TIME_SIMPLE() { +- $this->assertEquals('Y-m-d H:i:s', DateTime::DATE_TIME_SIMPLE); ++ public function test_DateTime_DATE_TIME_SIMPLE(): void { ++ $this->assertEquals('Y-m-d H:i:s', Formats::DATE_TIME_ISO_SHORT_FULL); +- public function test_Errors_E_INVALID_PASSWORD_HASH() { ++ public function test_Errors_E_INVALID_PASSWORD_HASH(): void { +- public function test_StringLiterals_AuthAccessDenied() { ++ public function test_StringLiterals_AuthAccessDenied(): void { +- public function test_StringLiterals_AuthAccessGranted() { ++ public function test_StringLiterals_AuthAccessGranted(): void { +- public function test_StringLiterals_HTTP_PREFIX() { ++ public function test_StringLiterals_HTTP_PREFIX(): void { +- public function test_StringLiterals_HTTPS_PREFIX() { ++ public function test_StringLiterals_HTTPS_PREFIX(): void { +============================================================ +========== tests/DomainTest.php +============================================================ +-use vhs\database\constraints\Constraint; +-use vhs\database\Table; +-use vhs\database\types\Type; ++use tests\domain\ExampleDomain; ++use tests\schema\ExampleSchema; +-use vhs\domain\Domain; +-use vhs\domain\Schema; +-use vhs\domain\validations\ValidationFailure; +-use vhs\domain\validations\ValidationResults; +-class ExampleSchema extends Schema { ++class DomainTest extends TestCase { +- public static function init() { +- $table = new Table('example', null); +- $table->addColumn('id', Type::Int()); +- $table->addColumn('testA', Type::String(true)); +- $table->addColumn('testB', Type::String(true)); +- $table->addColumn('testC', Type::String(true)); +- $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); +- return $table; +- } +-} +-class ExampleDomain extends Domain { +- ExampleDomain::Schema(ExampleSchema::Type()); +- } +- public function get_magic() { +- return 'magic field'; +- } +- public function get_testC() { +- return $this->internal_testC . 'fail'; +- } +- public function set_magic($value) { +- $this->testC = $value . 'magic'; +- } +- public function set_testC($value) { +- $this->internal_testC = $value . 'pass'; +- } +- public function validate(ValidationResults &$results) { +- if ($this->testA != 'pass') { +- $results->add(new ValidationFailure('testA is not equal to pass')); +- } +- } +-} +-class DomainTest extends TestCase { +- private static $mySqlEngine; +- public function stuff() { ++ public function stuff(): void { +- public function test_childRelationship() { ++ public function test_childRelationship(): void { +- public function test_InMemoryDomainTest() { ++ public function test_InMemoryDomainTest(): void { +- public function test_parentRelationship() { ++ public function test_parentRelationship(): void { +- $knight = \tests\domain\Knight::where(Where::Equal(\tests\domain\Knight::Schema()->Columns()->name, 'Black Knight')); ++ $knight = \tests\domain\Knight::where( ++ Where::Equal(\tests\domain\Knight::Schema()->Columns()->name, 'Black Knight') ++ ); +- public function test_satelliteRelationship() { ++ public function test_satelliteRelationship(): void { +- protected function setUp(): void { +- } ++ protected function setUp(): void {} +- protected function tearDown(): void { +- } ++ protected function tearDown(): void {} +============================================================ +========== tests/EmailTemplateDomainTest.php +============================================================ +- private $ids = []; +- public function test_Service() { ++ public function test_Service(): void { +- $rows = $service->ListTemplates(1, 1, 'id', 'id', ''); ++ $rows = array_map(fn($row): array => get_object_vars($row), array: json_decode(json_encode($service->ListTemplates(1, 1, 'id', 'id', '')))); +- $rows = $service->ListTemplates(1, 1, 'id', 'id', ''); ++ $rows = array_map(fn($row): array => get_object_vars($row), json_decode(json_encode($service->ListTemplates(1, 1, 'id', 'id', '')))); +- public function test_Template() { ++ public function test_Template(): void { +- $this->assertEquals('the value for a some other value asdf', $generated['subject']); +- $this->assertEquals('the value for a some other value qwer', $generated['txt']); +- $this->assertEquals('the value for a some other value zxcv', $generated['html']); ++ $this->assertEquals('the value for a some other value asdf', $generated->subject); ++ $this->assertEquals('the value for a some other value qwer', $generated->txt); ++ $this->assertEquals('the value for a some other value zxcv', $generated->html); +============================================================ +========== tests/KeyDomainTest.php +============================================================ ++use app\dto\UserActiveEnum; +- public function test_bullshitPhp() { ++ public function test_bullshitPhp(): void { +- public function test_Privileges() { ++ public function test_Privileges(): void { +- $user->active = 'y'; ++ $user->active = UserActiveEnum::ACTIVE->value; +============================================================ +========== tests/LimitTest.php +============================================================ +-use vhs\database\Table; +-use vhs\database\types\Type; +-use vhs\domain\Schema; +- public function test_EmptyLimit() { ++ public function test_EmptyLimit(): void { +- public function test_EmptyOffset() { ++ public function test_EmptyOffset(): void { +- public static function tearDownAfterClass(): void { +- } ++ public static function tearDownAfterClass(): void {} +- public function tearDown(): void { +- } ++ public function tearDown(): void {} +============================================================ +========== tests/ServiceTest.php +============================================================ ++use tests\security\PermPrincipal; +-use vhs\services\ServiceHandler; +-class PermPrincipal implements \vhs\security\IPrincipal { +- private $perms; +- public function __construct(...$perms) { +- if (is_null($perms)) { +- $perms = []; +- } +- $this->perms = $perms; +- } +- public function canGrantAllPermissions(...$permission) { +- } +- public function canGrantAnyPermissions(...$permission) { +- } +- public function getIdentity() { +- return null; +- } +- public function hasAllPermissions(...$permission) { +- return count(array_diff($permission, $this->perms)) == 0; +- } +- public function hasAnyPermissions(...$permission) { +- return count(array_intersect($permission, $this->perms)) > 0; +- } +- public function isAnon() { +- return false; +- } +- public function __toString() { +- return 'perm'; +- } +-} +- ServiceRegistry::register($logger, 'web', 'tests\\endpoints\\web', dirname(__FILE__) . '/..'); +- ServiceRegistry::register($logger, 'native', 'tests\\endpoints\\native', dirname(__FILE__) . '/..'); +- } +- public static function tearDownAfterClass(): void { +- } +- protected function setUp(): void { +- } ++ ServiceRegistry::register($logger, 'web', 'tests\\endpoints\\web', \vhs\BasePath::getBasePath(false)); ++ ServiceRegistry::register($logger, 'native', 'tests\\endpoints\\native', \vhs\BasePath::getBasePath(false)); ++ } ++ public static function tearDownAfterClass(): void {} ++ protected function setUp(): void {} +============================================================ +========== tests/WhereTest.php +============================================================ +- public function test_And() { ++ public function test_And(): void { +- public function test_AndOr() { ++ public function test_AndOr(): void { +- public function test_Equal() { ++ public function test_Equal(): void { +- public function test_Greater() { ++ public function test_Greater(): void { +- public function test_GreaterEqual() { ++ public function test_GreaterEqual(): void { +- public function test_In() { ++ public function test_In(): void { +- public function test_Lesser() { ++ public function test_Lesser(): void { +- public function test_LesserEqual() { ++ public function test_LesserEqual(): void { +- public function test_NotEqual() { ++ public function test_NotEqual(): void { +- public function test_NotIn() { ++ public function test_NotIn(): void { +- public function test_NotNull() { ++ public function test_NotNull(): void { +- public function test_Null() { ++ public function test_Null(): void { +- public function test_Or() { ++ public function test_Or(): void { +- public static function tearDownAfterClass(): void { +- } ++ public static function tearDownAfterClass(): void {} +- public function tearDown(): void { +- } ++ public function tearDown(): void {} +============================================================ +========== tests/autoload.php +============================================================ +-\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('tests', dirname(__FILE__) . '/..')); +-\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', dirname(__FILE__) . '/..')); ++define('DEBUG', false); ++\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('tests', \vhs\BasePath::getBasePath(false))); ++\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', \vhs\BasePath::getBasePath(false))); +============================================================ +========== tests/contracts/ITestService1.php +============================================================ +- public function NoDocMethod(); ++ public function NoDocMethod(): string; +============================================================ +========== tests/schema/KnightSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->swordid, SwordSchema::Table(), SwordSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->swordid, ++ SwordSchema::Table(), ++ SwordSchema::Columns()->id ++ ) +============================================================ +========== tests/schema/RingSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->enchantmentid, EnchantmentSchema::Table(), EnchantmentSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->knightid, KnightSchema::Table(), KnightSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->enchantmentid, ++ EnchantmentSchema::Table(), ++ EnchantmentSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->knightid, ++ KnightSchema::Table(), ++ KnightSchema::Columns()->id ++ ) +============================================================ +========== tests/schema/SwordEnchantmentsSchema.php +============================================================ +- Constraint::ForeignKey($table->columns->swordid, SwordSchema::Table(), SwordSchema::Columns()->id), +- Constraint::ForeignKey($table->columns->enchantmentid, EnchantmentSchema::Table(), EnchantmentSchema::Columns()->id) ++ Constraint::ForeignKey( ++ $table->columns->swordid, ++ SwordSchema::Table(), ++ SwordSchema::Columns()->id ++ ), ++ Constraint::ForeignKey( ++ $table->columns->enchantmentid, ++ EnchantmentSchema::Table(), ++ EnchantmentSchema::Columns()->id ++ ) +============================================================ +========== tests/services/TestService.php +============================================================ +- public function NoDocMethod() { ++ public function NoDocMethod(): string { +============================================================ +========== vhs/Cloneable.php +============================================================ +- public function __clone() { ++ public function __clone(): void { +============================================================ +========== vhs/Singleton.php +============================================================ +- protected function __construct() { +- } ++ protected function __construct() {} +- private function __clone() { +- } ++ public function __clone(): void {} +============================================================ +========== vhs/SplClassLoader.php +============================================================ +- protected function __construct() { +- } ++ protected function __construct() {} +- private function __clone() { +- } ++ public function __clone(): void {} +============================================================ +========== vhs/database/Column.php +============================================================ +- public function __clone() { ++ public function __clone(): void { +- public function __serialize() { +- return $this->serialize(); ++ public function __serialize(): array { ++ return [$this->serialize()]; +============================================================ +========== vhs/database/Database.php +============================================================ +-use vhs\database\queries\Query; +============================================================ +========== vhs/database/Element.php +============================================================ +- protected function __updateTable(Table &$table) { +- } ++ protected function __updateTable(Table &$table) {} +============================================================ +========== vhs/database/IConverter.php +============================================================ +-interface IConverter { +-} ++interface IConverter {} +============================================================ +========== vhs/database/IDataInterface.php +============================================================ +-use vhs\database\queries\Query; +============================================================ +========== vhs/database/IGenerator.php +============================================================ +-interface IGenerator { +-} ++interface IGenerator {} +============================================================ +========== vhs/database/ITableGenerator.php +============================================================ +- public function generateTable(Table $ascending); ++ public function generateTable(Table $table); +============================================================ +========== vhs/database/Table.php +============================================================ +- public function __clone() { ++ public function __clone(): void { +============================================================ +========== vhs/database/access/IAccessGenerator.php +============================================================ +-interface IAccessGenerator { +-} ++interface IAccessGenerator {} +============================================================ +========== vhs/database/constraints/ForeignKey.php +============================================================ +- public function __clone() { ++ public function __clone(): void { +============================================================ +========== vhs/database/engines/memory/InMemoryEngine.php +============================================================ +-use vhs\database\Column; +-use vhs\database\Columns; +-use vhs\database\orders\OrderBy; +-use vhs\database\queries\Query; +-use vhs\database\Table; +-use vhs\database\wheres\Where; +- if (isset($orderBy) || isset($limit)) { +- throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); +- } +- if (isset($orderBy) || isset($limit)) { +- throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); +- } +============================================================ +========== vhs/database/engines/memory/InMemoryGenerator.php +============================================================ +- public function generateBool(TypeBool $type, $value = null) { ++ public function generateBool(TypeBool $type, mixed $value = null) { +- public function generateDate(TypeDate $type, $value = null) { ++ public function generateDate(TypeDate $type, mixed $value = null) { +- public function generateDateTime(TypeDateTime $type, $value = null) { ++ public function generateDateTime(TypeDateTime $type, mixed $value = null) { +- public function generateEnum(TypeEnum $type, $value = null) { ++ public function generateEnum(TypeEnum $type, mixed $value = null) { +- public function generateFloat(TypeFloat $type, $value = null) { ++ public function generateFloat(TypeFloat $type, mixed $value = null) { +- public function generateInt(TypeInt $type, $value = null) { ++ public function generateInt(TypeInt $type, mixed $value = null) { +- public function generateTable(Table $ascending) { ++ public function generateTable(Table $table) { +- public function generateText(TypeText $type, $value = null) { ++ public function generateText(TypeText $type, mixed $value = null) { +============================================================ +========== vhs/database/engines/mysql/MySqlConverter.php +============================================================ +- if (is_null($value)) { ++ if (is_null($value) || empty($value)) { +============================================================ +========== vhs/database/engines/mysql/MySqlEngine.php +============================================================ +-use vhs\database\Columns; +-use vhs\database\limits\Limit; +-use vhs\database\offsets\Offset; +-use vhs\database\orders\OrderBy; +-use vhs\database\queries\Query; +-use vhs\database\Table; +-use vhs\database\types\Type; +-use vhs\database\wheres\Where; +============================================================ +========== vhs/database/engines/mysql/MySqlGenerator.php +============================================================ +-use vhs\database\queries\Query; +============================================================ +========== vhs/database/exceptions/DatabaseConnectionException.php +============================================================ +-class DatabaseConnectionException extends DatabaseException { +-} ++class DatabaseConnectionException extends DatabaseException {} +============================================================ +========== vhs/database/exceptions/DatabaseException.php +============================================================ +-class DatabaseException extends \Exception { +-} ++class DatabaseException extends \Exception {} +============================================================ +========== vhs/database/exceptions/InvalidSchemaException.php +============================================================ +-class InvalidSchemaException extends \Exception { +-} ++class InvalidSchemaException extends \Exception {} +============================================================ +========== vhs/database/joins/Join.php +============================================================ +- public function __clone() { ++ public function __clone(): void { +============================================================ +========== vhs/database/offsets/Offset.php +============================================================ +-use vhs\database\IGenerator; +- public function generate(IGenerator $generator, $value = null) { ++ public function generate($generator, $value = null) { +- private function generateOffset(IOffsetGenerator $generator) { ++ private function generateOffset($generator) { +============================================================ +========== vhs/database/queries/Query.php +============================================================ +-use vhs\database\joins\JoinCross; +-use vhs\database\joins\JoinInner; +- public $joins; ++ public array|null $joins = null; +- public function __construct(Table $table, Where $where = null) { ++ public function __construct(Table $table, ?Where $where = null) { +- public static function Count(Table $table, Where $where = null, OrderBy $orderBy = null, Limit $limit = null, Offset $offset = null) { ++ public static function Count(Table $table, ?Where $where = null, ?OrderBy $orderBy = null, ?Limit $limit = null, ?Offset $offset = null) { +- public static function Delete(Table $table, Where $where = null) { ++ public static function Delete(Table $table, ?Where $where = null) { +- Columns $columns = null, +- Where $where = null, +- OrderBy $orderBy = null, +- Limit $limit = null, +- Offset $offset = null ++ ?Columns $columns = null, ++ ?Where $where = null, ++ ?OrderBy $orderBy = null, ++ ?Limit $limit = null, ++ ?Offset $offset = null +============================================================ +========== vhs/database/queries/QueryCount.php +============================================================ ++use vhs\database\IGenerator; +- public function __construct(Table $table, Where $where = null, OrderBy $orderBy = null, Limit $limit = null, Offset $offset = null) { ++ public function __construct(Table $table, ?Where $where = null, ?OrderBy $orderBy = null, ?Limit $limit = null, ?Offset $offset = null) { +- public function generateQuery(IQueryGenerator $generator) { ++ public function generateQuery(IGenerator $generator) { +============================================================ +========== vhs/database/queries/QueryDelete.php +============================================================ +-use vhs\database\Table; +-use vhs\database\wheres\Where; +============================================================ +========== vhs/database/queries/QueryInsert.php +============================================================ +-use vhs\database\wheres\Where; +- public function __construct(Table $table, Columns $columns, array $values) { ++ public function __construct(Table $table, Columns $columns, $values = null) { +============================================================ +========== vhs/database/queries/QuerySelect.php +============================================================ ++use vhs\database\Column; +- Columns $columns = null, +- Where $where = null, +- OrderBy $orderBy = null, +- Limit $limit = null, +- Offset $offset = null ++ mixed $columns = null, ++ ?Where $where = null, ++ ?OrderBy $orderBy = null, ++ ?Limit $limit = null, ++ ?Offset $offset = null +============================================================ +========== vhs/database/queries/QueryUpdate.php +============================================================ +- public function __construct(Table $table, Columns $columns, Where $where = null, array $values) { ++ public function __construct(Table $table, Columns $columns, ?Where $where = null, $values = null) { +============================================================ +========== vhs/database/types/TypeEnum.php +============================================================ +- public $values; ++ public array $values; +- public function __construct(...$values) { +- if (is_null($values) || count($values) <= 0) { ++ public function __construct(string ...$values) { ++ if (gettype($values) !== 'array' || empty($values)) { +- $this->values = $values; ++ $this->values = array_values($values); +============================================================ +========== vhs/database/wheres/Where.php +============================================================ +-use vhs\database\queries\Query; +- public static function _And(Where ...$where) { ++ public static function _And(Where|null ...$where) { +============================================================ +========== vhs/database/wheres/WhereAnd.php +============================================================ +- public function __construct(Where ...$where) { ++ public function __construct(Where|null ...$where) { +============================================================ +========== vhs/domain/Domain.php +============================================================ +-use vhs\database\orders\OrderByAscending; +-interface IDomain { +-} +- private $__collections; ++ private array $__collections; +- public static function count($filters, array $allowed_columns = null) { ++ public static function coerceFilters(&$filters) { ++ if (is_string($filters)) { ++ $filters = json_decode($filters); ++ } ++ } ++ public static function count($filters, ?array $allowed_columns = null) { ++ Domain::coerceFilters($filters); +- public static function doCount(Where $where = null) { +- $class = get_called_class(); ++ public static function doCount(?Where $where = null) { +- public static function page($page, $size, $columns, $order, $filters, array $allowed_columns = null) { ++ public static function page($page, $size, $columns, $order, $filters, ?array $allowed_columns = null) { ++ Domain::coerceFilters($filters); +- if ($allowed_columns != null && !empty($allowed_columns)) { ++ if (is_array($allowed_columns) && !empty($allowed_columns)) { +- public static function Schema(Schema $schema = null) { ++ public static function Schema(?Schema $schema = null) { +- public static function where(Where $where = null, OrderBy $orderBy = null, $limit = null, $offset = null) { ++ public static function where(?Where $where = null, ?OrderBy $orderBy = null, $limit = null, $offset = null) { +- protected static function hydrateMany(Where $where = null, OrderBy $orderBy = null, $limit = null, $offset = null) { ++ protected static function hydrateMany(?Where $where = null, ?OrderBy $orderBy = null, $limit = null, $offset = null) { +- protected static function Relationship($as, $domain, Schema $joinTable = null) { ++ protected static function Relationship($as, $domain, ?Schema $joinTable = null) { +- private static function constructFilterWhere($filters, array $allowed_columns = null) { ++ private static function constructFilterWhere($filters, ?array $allowed_columns = null) { +- public function serialize() { ++ public function serialize(): string { +- public function unserialize($data) { ++ public function unserialize($data): void { +- return serialize($this->getInternalData()); ++ return $this->getInternalData(); +- return json_encode($data); ++ $result = json_encode($data); ++ if (!is_string($result)) { ++ throw new \Exception('Failed to convert to string'); ++ } ++ return $result; +============================================================ +========== vhs/domain/Schema.php +============================================================ +-use vhs\database\Table; +-interface ISchema { ++abstract class Schema implements ISchema { +- public static function init(); +-} +-abstract class Schema implements ISchema { +- private function __clone() { +- } ++ public function __clone(): void {} +============================================================ +========== vhs/domain/collections/ChildDomainCollection.php +============================================================ +-use vhs\database\constraints\PrimaryKey; +============================================================ +========== vhs/domain/collections/SatelliteDomainCollection.php +============================================================ +-use vhs\database\constraints\ForeignKey; +============================================================ +========== vhs/domain/exceptions/DomainException.php +============================================================ +-class DomainException extends \Exception { +-} ++class DomainException extends \Exception {} +============================================================ +========== vhs/domain/exceptions/InvalidColumnDefinitionException.php +============================================================ +-class InvalidColumnDefinitionException extends DomainException { +-} ++class InvalidColumnDefinitionException extends DomainException {} +============================================================ +========== vhs/loggers/StringLogger.php +============================================================ +- public $history; ++ public array $history; +============================================================ +========== vhs/messaging/MessageQueue.php +============================================================ +-use vhs\messaging\Engine; +- $mq->invokeEngine(function () use ($mq, $channel, $queue, $callback) { ++ $mq->invokeEngine(function () use ($mq, $channel, $queue, $callback): string { +- return $mq->engine->ensure($channel, $queue); ++ $mq->engine->ensure($channel, $queue); +- return $mq->engine->publish($channel, $queue, $message); ++ $mq->engine->publish($channel, $queue, $message); +============================================================ +========== vhs/messaging/engines/RabbitMQ/RabbitMQEngine.php +============================================================ +-use vhs\messaging\engines\RabbitMQ\RabbitMQConnectionInfo; +- return $ch->basic_consume($channel . '.' . $queue, function ($msg) use ($callback) { ++ return $ch->basic_consume($channel . '.' . $queue, \uniqid(), false, false, false, false, function ($msg) use ($callback) { +- return $ch->basic_publish(new AMQPMessage($message), '', $channel . '.' . $queue); ++ $ch->basic_publish(new AMQPMessage($message), '', $channel . '.' . $queue); +============================================================ +========== vhs/messaging/exceptions/MessageQueueException.php +============================================================ +-class MessageQueueException extends \Exception { +-} ++class MessageQueueException extends \Exception {} +============================================================ +========== vhs/migration/Backup.php +============================================================ +- public function __construct($server, $user, $password, $database, Logger $logger = null) { ++ public function __construct($server, $user, $password, $database, ?Logger $logger = null) { +- $fileName = !is_null($fileName) ? $fileName : 'db-backup-' . time() . '.sql'; ++ $fileName = !is_null($fileName) ? $fileName : sprintf('db-backup-%s.sql', time()); +- $command[] = "-u '" . $this->user . "'"; +- $command[] = '-p' . $this->password; ++ $command[] = sprintf("-u '%s'", $this->user); ++ $command[] = sprintf("-p '%s'", $this->password); +- $command[] = '--host ' . $this->server; ++ $command[] = '--host'; ++ $command[] = $this->server; ++ $command[] = '--skip-ssl-verify-server-cert'; +- $command[] = "'" . $this->database . "'"; ++ $command[] = sprintf("'%s'", $this->database); +- $command[] = "'" . $backupPath . $fileName . "'"; ++ $command[] = sprintf("'%s%s'", $backupPath, $fileName); +- if (!$return) { +- return true; +- } else { +- return false; +- } ++ return !$return; +- if (!$result) { ++ if (!$create_result) { +- break; +============================================================ +========== vhs/migration/Migrator.php +============================================================ ++ private array $cmd_opts = []; +- public function __construct($server, $user, $password, $database, Logger $logger = null) { ++ public function __construct($server, $user, $password, $database, ?Logger $logger = null) { ++ if (getenv('MIGRATOR_SKIP_SSL', true) !== false) { ++ array_push($this->cmd_opts, '--skip-ssl'); ++ } +- $command = 'mysql -u' . DB_USER . ' -p' . DB_PASS . ' ' . '-h ' . DB_SERVER . ' -D ' . DB_DATABASE . " < {$script_path}"; ++ $command = sprintf( ++ "mysql -u %s -p %s -h %s -D %s %s < %s\n", ++ DB_USER, ++ DB_PASS, ++ DB_SERVER, ++ DB_DATABASE, ++ implode(' ', $this->cmd_opts), ++ $script_path ++ ); +============================================================ +========== vhs/monitors/Monitor.php +============================================================ +- abstract public function Init(Logger &$logger = null); ++ abstract public function Init(?Logger &$logger = null); +============================================================ +========== vhs/security/CurrentUser.php +============================================================ +-use vhs\Singleton; ++ public $currentPrincipal; +- private $currentPrincipal; +- private function __clone() { +- } ++ public function __clone(): void {} +============================================================ +========== vhs/security/ICredentials.php +============================================================ +-interface ICredentials { +-} ++interface ICredentials {} +============================================================ +========== vhs/security/SystemPrincipal.php +============================================================ +-use vhs\security\IPrincipal; +- public function __construct() { +- } ++ public function __construct() {} +============================================================ +========== vhs/security/exceptions/InvalidCredentials.php +============================================================ +-class InvalidCredentials extends \Exception { ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; ++class InvalidCredentials extends HttpException { ++ public function __construct($message = 'Access denied', $code = HttpStatusCodes::Client_Error_Unauthorized) { ++ parent::__construct($message, $code); ++ } +============================================================ +========== vhs/security/exceptions/UnauthorizedException.php +============================================================ +-class UnauthorizedException extends \Exception { ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; ++class UnauthorizedException extends HttpException { +- parent::__construct($message); ++ parent::__construct($message, HttpStatusCodes::Client_Error_Forbidden); +============================================================ +========== vhs/services/IContract.php +============================================================ +-interface IContract { +-} ++interface IContract {} +============================================================ +========== vhs/services/Service.php +============================================================ ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; +- public function __construct(ServiceContext $context = null) { ++ public function __construct(?ServiceContext $context = null) { ++ protected function throwNotFound() { ++ $className = get_called_class(); ++ throw new HttpException(sprintf('%s not found', $className), HttpStatusCodes::Client_Error_Not_Found); ++ } +============================================================ +========== vhs/services/ServiceClient.php +============================================================ +- $uri = '/services/' . $namespace . '/' . $service . '.svc'; ++ $uri = "/services/$namespace/$service.svc"; +============================================================ +========== vhs/services/ServiceHandler.php +============================================================ ++use vhs\web\enums\HttpStatusCodes; ++ $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('handling: %s with prefixpath %s', $uri, $this->uriPrefixPath)); +- throw new InvalidRequestException('Invalid service request'); ++ $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('did not find match for: %s', $uri)); ++ throw new InvalidRequestException('Invalid service request', HttpStatusCodes::Client_Error_Misdirected_Request); ++ $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('$endpoints => %s', json_encode($endpoints))); +- throw new InvalidRequestException('Invalid service request'); ++ throw new InvalidRequestException('Invalid endpoint request', HttpStatusCodes::Client_Error_Im_a_teapot); +- $endpoint = $this->endpointNamespace . '\\' . $regs['endpoint']; +- return $endpoint; ++ return $this->endpointNamespace . '\\' . $regs['endpoint']; +============================================================ +========== vhs/services/endpoints/Endpoint.php +============================================================ +-use vhs\Logger; +- private function __clone() { +- } ++ public function __clone(): void {} +============================================================ +========== vhs/services/exceptions/InvalidContractException.php +============================================================ +-class InvalidContractException extends \Exception { +-} ++class InvalidContractException extends \Exception {} +============================================================ +========== vhs/services/exceptions/InvalidRequestException.php +============================================================ +-class InvalidRequestException extends \Exception { ++use vhs\exceptions\HttpException; ++use vhs\web\enums\HttpStatusCodes; ++class InvalidRequestException extends HttpException { ++ public function __construct( ++ $message = 'Invalid Request Exception', ++ HttpStatusCodes $code = HttpStatusCodes::Client_Error_Method_Not_Allowed, ++ $previous = null ++ ) { ++ parent::__construct($message, $code, $previous); ++ } +============================================================ +========== vhs/web/HttpContext.php +============================================================ +- private static $server; ++ private static $server = null; +============================================================ +========== vhs/web/HttpRequest.php +============================================================ +- public function __construct() { +- } ++ public function __construct() {} +============================================================ +========== vhs/web/HttpServer.php +============================================================ ++use vhs\services\exceptions\InvalidRequestException; ++use vhs\web\enums\HttpStatusCodes; ++ public $logger; +- private $logger; +- public function __construct(HttpServerInfoModule $infoModule = null, Logger &$logger = null) { ++ public function __construct(?HttpServerInfoModule $infoModule = null, ?Logger &$logger = null) { +- $this->http_response_code = 200; ++ $this->http_response_code = HttpStatusCodes::Success_Ok->value; ++ $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'trying end'); ++ $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'already ended - bailing'); ++ $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); ++ $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('trying module: %s', get_class($module))); ++ if (!$this->endset && is_null($exception)) { ++ $exception = new InvalidRequestException('No valid service endpoint found', HttpStatusCodes::Server_Error_Service_Unavailable); ++ } +- public function header($string, $replace = true, $http_response_code = null) { ++ public function header($string, $replace = true, $http_response_code = 0) { +- array_push($this->headerBuffer, function () use ($self, $string, $replace, $http_response_code) { ++ array_push($this->headerBuffer, function () use ($string, $replace, $http_response_code) { +- array_push($this->headerBuffer, function () use ($self) { ++ array_push($this->headerBuffer, function (): never { +============================================================ +========== vhs/web/modules/HttpBasicAuthModule.php +============================================================ +-use vhs\web\HttpBasicCredentials; +- public function endResponse(HttpServer $server) { +- } ++ public function endResponse(HttpServer $server) {} +============================================================ +========== vhs/web/modules/HttpBearerTokenAuthModule.php +============================================================ +-use vhs\security\UserPassCredentials; +-use vhs\web\HttpBasicCredentials; +- public function endResponse(HttpServer $server) { +- } ++ public function endResponse(HttpServer $server) {} +- public function handleException(HttpServer $server, \Exception $ex) { +- } ++ public function handleException(HttpServer $server, \Exception $ex) {} +============================================================ +========== vhs/web/modules/HttpExceptionHandlerModule.php +============================================================ +- private $logger; ++ private $logger = null; +- public function endResponse(HttpServer $server) { +- } ++ public function endResponse(HttpServer $server) {} +- public function handle(HttpServer $server) { +- } ++ public function handle(HttpServer $server) {} ++ $server->code($ex->getCode() !== 0 ? $ex->getCode() : 500); +============================================================ +========== vhs/web/modules/HttpJsonServiceHandlerModule.php +============================================================ ++use vhs\web\enums\HttpStatusCodes; +- public function endResponse(HttpServer $server) { +- } ++ public function endResponse(HttpServer $server) {} +- switch ($server->request->method) { +- case 'HEAD': +- $server->output(ServiceRegistry::get($this->registryKey)->discover($uri)); +- break; +- case 'GET': +- if (isset($_GET['json'])) { +- $input = $_GET['json']; +- } else { +- $input = json_encode($_GET); +- } +- $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, $input)); +- break; +- case 'POST': +- $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, file_get_contents('php://input'))); +- break; +- default: +- throw new InvalidRequestException(); +- break; ++ try { ++ switch ($server->request->method) { ++ case 'HEAD': ++ $server->output(ServiceRegistry::get($this->registryKey)->discover($uri)); ++ $server->end(); ++ break; ++ case 'GET': ++ if (isset($_GET['json'])) { ++ $input = $_GET['json']; ++ } else { ++ $input = json_encode($_GET); ++ } ++ $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, $input)); ++ break; ++ case 'POST': ++ $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, file_get_contents('php://input'))); ++ $server->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); ++ break; ++ default: ++ throw new InvalidRequestException(); ++ } ++ $server->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); ++ $server->end(); ++ } catch (\Throwable $exception) { ++ $server->logger->debug( ++ __FILE__, ++ __LINE__, ++ __METHOD__, ++ sprintf('caught exception: %s(%s)', $exception->getMessage(), $exception->getCode()) ++ ); ++ if ($exception->getCode() !== HttpStatusCodes::Client_Error_Misdirected_Request->value) { ++ throw $exception; ++ } +- public function handleException(HttpServer $server, \Exception $ex) { +- } ++ public function handleException(HttpServer $server, \Exception $ex) {} +============================================================ +========== vhs/web/modules/HttpRequestHandlerModule.php +============================================================ +- public function __construct() { +- } ++ public function __construct() {} +- public function endResponse(HttpServer $server) { +- } ++ public function endResponse(HttpServer $server) {} +- public function handleException(HttpServer $server, \Exception $ex) { +- } ++ public function handleException(HttpServer $server, \Exception $ex) {} +============================================================ +========== vhs/web/modules/HttpServerInfoModule.php +============================================================ +- public function endResponse(HttpServer $server) { +- } ++ public function endResponse(HttpServer $server) {} +- public function handleException(HttpServer $server, \Exception $ex) { +- } ++ public function handleException(HttpServer $server, \Exception $ex) {} diff --git a/docker-compose.dev.conf b/docker-compose.dev.conf index 9e34f749..6d8baa5b 100644 --- a/docker-compose.dev.conf +++ b/docker-compose.dev.conf @@ -5,41 +5,51 @@ ## ## ################################################################################################### -## Always use the core -docker-compose/core.yml +## Always use the backend +docker-compose/backend/php/base.yml + +## +## Choose your frontend +## +## NOTE: ALWAYS CHOOSE ONE +## +# docker-compose/frontend/web/base.yml +# docker-compose/frontend/web/build.yml +docker-compose/frontend/react/base.yml +docker-compose/frontend/react/build.yml ## ## Expose port ## -# docker-compose/core.ports.yml +# docker-compose/frontend/react/ports.yml ## ## Lift environments from nomos.env file ## -docker-compose/files-local-nomos-env.yml +docker-compose/backend/php/files-local-nomos-env.yml ## -## Build core +## Build common ## -docker-compose/build-backend.yml -docker-compose/build-frontend.yml +docker-compose/backend/php/build.yml + ## -## Enable bridge network_mode for core services +## Enable bridge network_mode for common services ## -#docker-compose/core.network-bridge.yml +# docker-compose/common/react/network-bridge.yml ## -## Enable proxy network for core services +## Enable proxy network for common services ## -#docker-compose/core.network-proxy.yml +# docker-compose/common/react/network-proxy.yml ## -## Enable proxy network for core services, managed by docker-compose +## Enable proxy network for common services, managed by docker-compose ## -docker-compose/core.network-proxy-internal.yml +docker-compose/common/react/network-proxy-internal.yml ## ## MySQL @@ -49,27 +59,27 @@ docker-compose/core.network-proxy-internal.yml ## Local MySQL instance ## -docker-compose/mysql-local.yml -#docker-compose/mysql-local-network-bridge.yml -docker-compose/mysql-local-network-mysql.yml +docker-compose/mysql/local.yml +# docker-compose/mysql/local-network-bridge.yml +docker-compose/mysql/local-network-mysql.yml ## ## External MySQL instance ## -# docker-compose/mysql-external-mysqld.yml +# docker-compose/mysql/external-mysqld.yml ## ## External MySQL network (mysql) ## -# docker-compose/mysql-external-network-mysql.yml +# docker-compose/mysql/external-network-mysql.yml ## ## Inject the MySQL container through EXTERNAL_MYSQL_HOST ## -# docker-compose/mysql-external-variable.yml +# docker-compose/mysql/external-variable.yml ## ## Webhooker @@ -77,57 +87,57 @@ docker-compose/mysql-local-network-mysql.yml ## Webhooker depends on RabbitMQ, so always enable a RabbitMQ ## -docker-compose/webhooker.yml +docker-compose/webhooker/base.yml ## ## Webhooker logs ## -docker-compose/webhooker-logs-local.yml +docker-compose/webhooker/logs-local.yml ## ## Build webhooker ## -docker-compose/webhooker-build.yml +docker-compose/webhooker/build.yml ## ## Enable bridge network_mode for webhooker ## -# docker-compose/webhooker-network-bridge.yml +# docker-compose/webhooker/network-bridge.yml ## ## Enable proxy network for webhooker ## -# docker-compose/webhooker-network-proxy.yml +# docker-compose/webhooker/network-proxy.yml ## ## Enable rabbitmq network for webhooker ## -# docker-compose/webhooker-network-rabbitmq.yml +# docker-compose/webhooker/network-rabbitmq.yml ## ## Local RabbitMQ instance ## -docker-compose/rabbitmq-local.yml -docker-compose/rabbitmq-local-management.yml -# docker-compose/rabbitmq-local-network-bridge.yml +docker-compose/rabbitmq/local.yml +docker-compose/rabbitmq/local-management.yml +# docker-compose/rabbitmq/local-network-bridge.yml -# Network not managed with docker-compose -#docker-compose/rabbitmq-local-network-rabbitmq.yml +## Network not managed with docker-compose +# docker-compose/rabbitmq/local-network-rabbitmq.yml -# Network managed with docker-compose -docker-compose/rabbitmq-local-network-internal.yml +## Network managed with docker-compose +docker-compose/rabbitmq/local-network-internal.yml ## ## External RabbitMQ instance ## -# docker-compose/rabbitmq-external.yml +# docker-compose/rabbitmq/external.yml ## ## Vhosts @@ -139,22 +149,23 @@ docker-compose/rabbitmq-local-network-internal.yml ## Enable membership.vanhack.ca for nginx-proxy ## -# docker-compose/vhost-membership-vanhack-ca.yml +# docker-compose/frontend/react/nginx-proxy-prod.yml ## ## Enable membership.test.vanhack.ca for nginx-proxy ## -# docker-compose/vhost-membership-test-vanhack-ca.yml +# docker-compose/frontend/react/nginx-proxy-devtest.yml ## ## Override backend files from local filesystem ## -docker-compose/files-local-backend.yml +docker-compose/backend/php/files-local.yml ## ## Override frontend files from local filesystem ## -docker-compose/files-local-frontend.yml +# docker-compose/frontend/react/files-local.yml +# docker-compose/frontend/web/files-local.yml diff --git a/docker-compose.sample.conf b/docker-compose.sample.conf index b8539400..cc6ab43c 100644 --- a/docker-compose.sample.conf +++ b/docker-compose.sample.conf @@ -4,150 +4,83 @@ ## ## ################################################################################################### -## Always use the core -docker-compose/core.yml - ## -## Expose port +## Backend Section ## -# docker-compose/core.ports.yml -## -## Lift environments from nomos.env file -## +### Base backend config +docker-compose/backend/php/base.yml -docker-compose/files-local-nomos-env.yml +### Lift environments from nomos.env file +docker-compose/backend/php/files-local-nomos-env.yml -## -## Build core -## +### Build backend +docker-compose/backend/php/build.yml -docker-compose/build-backend.yml -docker-compose/build-frontend.yml +### Override backend files from local filesystem +docker-compose/backend/php/files-local.yml ## -## Enable bridge network_mode for core services -## -# docker-compose/core.network-bridge.yml - +## Common Section ## -## Enable proxy network for core services +## (frontend/backend) shared configs ## -docker-compose/core.network-proxy.yml + +## Enable bridge network_mode for common services +# docker-compose/common/react/network-bridge.yml + +## Enable proxy network for common services +docker-compose/common/react/network-proxy.yml ## ## MySQL ## -## ## Local MySQL instance -## - -# docker-compose/mysql-local.yml -# docker-compose/mysql-local-network-bridge.yml -# docker-compose/mysql-local-network-mysql.yml +# docker-compose/mysql/local.yml +# docker-compose/mysql/local-network-bridge.yml +# docker-compose/mysql/local-network-mysql.yml -## ## External MySQL instance -## - -docker-compose/mysql-external-mysqld.yml +docker-compose/mysql/external-mysqld.yml -## ## External MySQL network (mysql) -## +docker-compose/mysql/external-network-mysql.yml -docker-compose/mysql-external-network-mysql.yml - -## ## Inject the MySQL container through EXTERNAL_MYSQL_HOST -## - -# docker-compose/mysql-external-variable.yml +# docker-compose/mysql/external-variable.yml ## -## Webhooker -## -## Webhooker depends on RabbitMQ, so always enable a RabbitMQ +## Webhooker Section ## -docker-compose/webhooker.yml +### Webhooker depends on RabbitMQ, so always enable a RabbitMQ +docker-compose/webhooker/base.yml -## -## Webhooker logs -## +### Webhooker logs +docker-compose/webhooker/logs-local.yml -docker-compose/webhooker-logs-local.yml +### Build webhooker +docker-compose/webhooker/build.yml -## -## Build webhooker -## +### Enable bridge network_mode for webhooker +# docker-compose/webhooker/network-bridge.yml -docker-compose/webhooker-build.yml +### Enable proxy network for webhooker +# docker-compose/webhooker/network-proxy.yml -## -## Enable bridge network_mode for webhooker -## - -# docker-compose/webhooker-network-bridge.yml +### Enable rabbitmq network for webhooker +docker-compose/webhooker/network-rabbitmq.yml ## -## Enable proxy network for webhooker +## RabbitMQ ## -# docker-compose/webhooker-network-proxy.yml - -## -## Enable rabbitmq network for webhooker -## - -docker-compose/webhooker-network-rabbitmq.yml - -## -## Local RabbitMQ instance -## - -docker-compose/rabbitmq-local.yml -docker-compose/rabbitmq-local-management.yml -# docker-compose/rabbitmq-local-network-bridge.yml -docker-compose/rabbitmq-local-network-rabbitmq.yml - -## -## External RabbitMQ instance -## - -# docker-compose/rabbitmq-external.yml - -## -## Vhosts -## - -## Enable membership.vanhack.ca - -## Traefik -# docker-compose/traefik-prod.yml - -## nginx-proxy -# docker-compose/nginx-proxy-prod.yml - -## -## Enable membership.test.vanhack.ca -## - -## Traefik -# docker-compose/traefik-devtest.yml - -## nginx-proxy -# docker-compose/nginx-proxy-devtest.yml - -## -## Override backend files from local filesystem -## - -docker-compose/files-local-backend.yml - -## -## Override frontend files from local filesystem -## +### Local RabbitMQ instance +docker-compose/rabbitmq/local.yml +docker-compose/rabbitmq/local-management.yml +# docker-compose/rabbitmq/local-network-bridge.yml +docker-compose/rabbitmq/local-network-rabbitmq.yml -docker-compose/files-local-frontend.yml +### External RabbitMQ instance +# docker-compose/rabbitmq/external.yml diff --git a/docker-compose.sh b/docker-compose.sh index 23b59838..33592e1e 100755 --- a/docker-compose.sh +++ b/docker-compose.sh @@ -17,4 +17,4 @@ export COMPOSE_FILE # container. # shellcheck disable=SC2086 -/usr/bin/env ${COMPOSE_CMD} -p nomos --env-file "${SCRIPT_DIR}/docker/nomos.env" "$@" +/usr/bin/env ${COMPOSE_CMD} --env-file "${SCRIPT_DIR}/docker/nomos.env" "$@" diff --git a/docker-compose.template.conf b/docker-compose.template.conf index ea2ca7f9..1ade2161 100644 --- a/docker-compose.template.conf +++ b/docker-compose.template.conf @@ -1,157 +1,170 @@ -## Always use the core -docker-compose/core.yml - ## -## Expose port +## Backend Section ## -# docker-compose/core.ports.yml -## -## Lift environments from nomos.env file -## +### Base backend config +docker-compose/backend/php/base.yml -#docker-compose/files-local-nomos-env.yml +### Lift environments from nomos.env file +docker-compose/backend/php/files-local-nomos-env.yml -## -## Build core -## +### Build backend +# docker-compose/backend/php/build.yml -#docker-compose/build-backend.yml -#docker-compose/build-frontend.yml +### Override backend files from local filesystem +# docker-compose/backend/php/files-local.yml ## -## Enable bridge network_mode for core services -## -# docker-compose/core.network-bridge.yml - +## Frontend Section ## -## Enable proxy network for core services +## Note: Change react to web for the old angular v1 frontend ## -#docker-compose/core.network-proxy.yml -## -## Enable proxy network for core services, managed by docker-compose -## -#docker-compose/core.network-proxy-internal.yml +### Base frontend config +docker-compose/frontend/react/base.yml -## -## MySQL -## +### Public ports +docker-compose/frontend/react/ports.yml -## -## Local MySQL instance -## +### Build frontend +# docker-compose/frontend/react/build.yml -# docker-compose/mysql-local.yml -# docker-compose/mysql-local-network-bridge.yml -# docker-compose/mysql-local-network-mysql.yml +### Override frontend files from local filesystem +# docker-compose/frontend/react/files-local.yml -## -## External MySQL instance -## +### +### Virtual host Section +### -#docker-compose/mysql-external-mysqld.yml +### Traefik - membership.vanhack.ca +# docker-compose/frontend/react/traefik-prod.yml -## -## External MySQL network (mysql) -## +### nginx-proxy - membership.vanhack.ca +# docker-compose/frontend/react/nginx-proxy-prod.yml -#docker-compose/mysql-external-network-mysql.yml +### Traefik - membership.devtest.vanhack.ca +# docker-compose/frontend/react/traefik-devtest.yml -## -## Inject the MySQL container through EXTERNAL_MYSQL_HOST -## +### nginx-proxy - membership.devtest.vanhack.ca +# docker-compose/frontend/react/nginx-proxy-devtest.yml -# docker-compose/mysql-external-variable.yml +### Enable membership.test.vanhack.ca -## -## Webhooker -## -## Webhooker depends on RabbitMQ, so always enable a RabbitMQ -## +### Traefik - membership-new.devtest.vanhack.ca +# docker-compose/frontend/react/traefik-devtest-new.yml -#docker-compose/webhooker.yml +### nginx-proxy - membership-new.devtest.vanhack.ca +# docker-compose/frontend/react/traefik-prod-new.yml ## -## Webhooker logs +## Enable bridge network_mode for common services ## - -#docker-compose/webhooker-logs-local.yml +# docker-compose/common/react/network-bridge.yml ## -## Build webhooker +## Enable proxy network for common services ## - -#docker-compose/webhooker-build.yml +# docker-compose/common/react/network-proxy.yml ## -## Enable bridge network_mode for webhooker +## Enable proxy network for common services, managed by docker-compose ## - -# docker-compose/webhooker-network-bridge.yml +# docker-compose/common/react/network-proxy-internal.yml ## -## Enable proxy network for webhooker +## MySQL ## -# docker-compose/webhooker-network-proxy.yml +### +### Local Instance +### +# docker-compose/mysql/local.yml + +#### Local networks +# docker-compose/mysql/local-network-bridge.yml +# docker-compose/mysql/local-network-mysql.yml + +### +### External instance +### + +#### instance hostname mysqld +# docker-compose/mysql/external-mysqld.yml + +#### Network options +# docker-compose/mysql/external-network-mysql.yml +# docker-compose/mysql/external-network-mysql-beryllium.yml +# docker-compose/mysql/external-network-mysql-lithium.yml + +#### A mysql daemon instance configured by variable +# docker-compose/mysql/external-variable.yml ## -## Enable rabbitmq network for webhooker +## Local MySQL instance ## -#docker-compose/webhooker-network-rabbitmq.yml +# docker-compose/mysql/local.yml +# docker-compose/mysql/local-network-bridge.yml +# docker-compose/mysql/local-network-mysql.yml ## -## Local RabbitMQ instance +## External MySQL instance ## -#docker-compose/rabbitmq-local.yml -#docker-compose/rabbitmq-local-management.yml -# docker-compose/rabbitmq-local-network-bridge.yml +# docker-compose/mysql/external-mysqld.yml -# Network not managed with docker-compose -#docker-compose/rabbitmq-local-network-rabbitmq.yml +## +## External MySQL network (mysql) +## -# Network managed with docker-compose -# docker-compose/rabbitmq-local-network-internal.yml +# docker-compose/mysql/external-network-mysql.yml ## -## External RabbitMQ instance +## Inject the MySQL container through EXTERNAL_MYSQL_HOST ## -# docker-compose/rabbitmq-external.yml +# docker-compose/mysql/external-variable.yml ## -## Vhosts +## RabbitMQ Section ## -## Enable membership.vanhack.ca +### Local/integrated RabbitMQ instance +# docker-compose/rabbitmq/local.yml + +### Enable management +# docker-compose/rabbitmq/local-management.yml -## Traefik -# docker-compose/traefik-prod.yml +### RabbitMQ network +# docker-compose/rabbitmq/local-network-bridge.yml +# docker-compose/rabbitmq/local-network-internal.yml +# docker-compose/rabbitmq/local-network-rabbitmq.yml -## nginx-proxy -# docker-compose/nginx-proxy-prod.yml +### External RabbitMQ instance +# docker-compose/rabbitmq/external.yml ## -## Enable membership.test.vanhack.ca +## Webhooker Section ## -## Traefik -# docker-compose/traefik-devtest.yml +### Base config +docker-compose/webhooker/base.yml -## nginx-proxy -# docker-compose/nginx-proxy-devtest.yml +### Build +# docker-compose/webhooker/build.yml -## -## Override backend files from local filesystem -## +### Local logs +# docker-compose/webhooker/logs-local.yml -#docker-compose/files-local-backend.yml +### Network +# docker-compose/webhooker/network-bridge.yml +# docker-compose/webhooker/network-proxy-internal.yml +# docker-compose/webhooker/network-proxy.yml +# docker-compose/webhooker/network-rabbitmq.yml ## -## Override frontend files from local filesystem +## Misc Section ## -#docker-compose/files-local-frontend.yml +### Overrides +# docker-compose/override.yml diff --git a/docker-compose/.dockerignore b/docker-compose/.dockerignore new file mode 100644 index 00000000..aa3a93b8 --- /dev/null +++ b/docker-compose/.dockerignore @@ -0,0 +1,5 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +dist diff --git a/docker-compose/.gitignore b/docker-compose/.gitignore new file mode 100644 index 00000000..af378769 --- /dev/null +++ b/docker-compose/.gitignore @@ -0,0 +1,13 @@ +* +!.gitignore + +!backend/ +!common/ +!frontend/ +!mysql/ +!rabbitmq/ +!webhooker/ + +!Dockerfile + +!override.sample.yml diff --git a/docker-compose/Dockerfile b/docker-compose/Dockerfile index 9c7d17dd..180080e9 100644 --- a/docker-compose/Dockerfile +++ b/docker-compose/Dockerfile @@ -1,66 +1,133 @@ ARG PHP_VERSION=8.3 -FROM cimg/php:${PHP_VERSION}-node AS build +FROM scratch AS source -USER 1001:1002 +WORKDIR /build -WORKDIR /build/ +COPY --link ./ ./ -ENV DEBIAN_FRONTEND=noninteractive +FROM node:22-alpine AS base -COPY --chown=circleci:circleci ./ ./ +ENV PNPM_HOME="/pnpm" -RUN npm ci && cd webhooker && npm ci +ENV PATH="$PNPM_HOME:$PATH" -FROM php:${PHP_VERSION}-fpm AS backend +RUN npm install -g npm@latest && npm install -g corepack@latest -VOLUME ["/sessions"] +FROM base AS build-base + +RUN apk --no-cache add just mariadb-client php83-bcmath php83-common php83-curl php83-dom php83-iconv php83-mbstring php83-mysqli php83-mysqlnd php83-openssl php83-phar php83-session php83-simplexml php83-sockets php83-zip php83 python3 wget alpine-sdk bash git php83-tokenizer php83-xml php83-xmlwriter + +FROM build-base AS build + +ENV COMPOSER_INSTALL_OPT=--no-dev + +ENV CI=true + +RUN mkdir /app/ /build/ && chown 1000:1000 /build/ /app/ + +USER 1000:1000 + +COPY --from=source --chown=1000:1000 --link /build /build + +WORKDIR /build + +RUN --mount=type=cache,id=nomos-pnpm,target=/pnpm/store,uid=1000,gid=1000 pnpm install --frozen-lockfile && pnpm -r build + +FROM scratch AS build-backend-php + +WORKDIR /build + +COPY --from=build --link /build/packages/backend-php/ /app/backend-php/ + +FROM scratch AS build-frontend-react + +WORKDIR /build + +COPY --from=build --link /build/packages/frontend-react/conf/ /app/frontend-react/conf/ + +COPY --from=build --link /build/packages/frontend-react/dist/ /app/frontend-react/dist/ + +FROM scratch AS build-frontend-web + +WORKDIR /build + +COPY --from=build --link /build/packages/frontend-web/ /app/frontend-web/ + +FROM build AS build-webhooker -RUN mkdir /sessions && chown www-data:www-data /sessions +WORKDIR /build -ENV DEBIAN_FRONTEND=noninteractive +RUN --mount=type=cache,id=nomos-pnpm,target=/pnpm/store,uid=1000,gid=1000 pnpm deploy --filter="@vhs/webhooker" --prod /app/webhooker/ + +FROM alpine:3 AS backend-php-base + +RUN apk --no-cache add just mariadb-client php83-bcmath php83-common php83-curl php83-dom php83-iconv php83-mbstring php83-mysqli php83-mysqlnd php83-openssl php83-phar php83-session php83-simplexml php83-sockets php83-zip php83 python3 wget php83-fpm + +RUN sed -i 's/^listen/;listen/g' /etc/php83/php-fpm.d/www.conf + +FROM base AS webhooker-base + +RUN apk --no-cache add bash + +FROM backend-php-base AS backend-php + +VOLUME ["/sessions"] + +EXPOSE 9000/tcp WORKDIR /var/www/html -ENTRYPOINT [ "docker_compose_run.sh" ] -CMD ["php-fpm"] +VOLUME ["/var/www/html/backup"] + +RUN mkdir -p /var/www/html/backup && chown nobody:nobody /var/www/html/backup -RUN apt-get update \ - && apt-get install -y git libzip-dev mariadb-client zip \ - && docker-php-ext-install -j$(nproc) bcmath mysqli zip \ - && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* +ENTRYPOINT ["docker_compose_run.sh"] -COPY --from=build /build/app/ app/ -COPY --from=build /build/migrations/ migrations/ -COPY --from=build /build/tools/ tools/ -COPY --from=build /build/vhs/ vhs/ -COPY --from=build /build/vendor/ vendor/ +CMD ["php-fpm83"] -COPY --from=build /build/docker/*.sh /usr/local/bin/ -COPY --from=build /build/conf/config.docker.ini.php /var/www/html/conf/config.ini.php +RUN mkdir -p /var/www/html/conf/ -RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ - && echo "session.save_path = \"/sessions\"" >> "$PHP_INI_DIR/php.ini" +COPY --from=source --link /build/migrations/ migrations/ -FROM nginx:stable-alpine AS frontend +COPY --from=build-backend-php --link /app/backend-php/app/ app/ -COPY conf/nginx-vhost-docker-compose.conf /etc/nginx/conf.d/default.conf +COPY --from=build-backend-php --link /app/backend-php/conf/config.docker.ini.php conf/config.ini.php -COPY --from=build /build/web/ /var/www/html/ +COPY --from=build-backend-php --link /app/backend-php/tools/ tools/ -FROM build AS webhooker-build +COPY --from=build-backend-php --link /app/backend-php/vhs/ vhs/ -WORKDIR /build/webhooker +COPY --from=build-backend-php --link /app/backend-php/vendor/ vendor/ -RUN chmod 755 webhooker.sbin webhooker.console \ - && cp config.docker.js config.js +COPY --from=build-backend-php --link /app/backend-php/conf/php/*.ini /etc/php83/conf.d/ -FROM node:lts AS webhooker +COPY --from=build-backend-php --link /app/backend-php/conf/php-fpm/*.conf /etc/php83/php-fpm.d/ -RUN mkdir -p /app/logs /app/webhooker +COPY --from=build-backend-php --link /app/backend-php/docker/*.sh /usr/local/bin/ -COPY --from=webhooker-build /build/webhooker/ /app/webhooker/ +FROM nginx:stable-alpine AS frontend-react + +COPY --from=build-frontend-react --link /app/frontend-react/conf/nginx-react-docker-compose.conf /etc/nginx/conf.d/default.conf + +COPY --from=build-frontend-react --link /app/frontend-react/dist/ /var/www/html/ + +FROM nginx:stable-alpine AS frontend-web + +COPY --from=build-frontend-web --link /app/frontend-web/conf/nginx-vhost-docker-compose.conf /etc/nginx/conf.d/default.conf + +COPY --from=build-frontend-web --link /app/frontend-web/web/ /var/www/html/ + +FROM webhooker-base AS webhooker + +USER 1000:1000 + +WORKDIR /app + +RUN mkdir -p /app/logs /app/webhooker && chown 1000:1000 /app/logs /app/webhooker WORKDIR /app/webhooker +COPY --from=build-webhooker --link /app/webhooker/ /app/webhooker/ + CMD ["/app/webhooker/webhooker.sbin"] diff --git a/docker-compose/backend/.gitignore b/docker-compose/backend/.gitignore new file mode 100644 index 00000000..cf988d0f --- /dev/null +++ b/docker-compose/backend/.gitignore @@ -0,0 +1,4 @@ +* +!.gitignore + +!php/ diff --git a/docker-compose/backend/php/.gitignore b/docker-compose/backend/php/.gitignore new file mode 100644 index 00000000..e7404404 --- /dev/null +++ b/docker-compose/backend/php/.gitignore @@ -0,0 +1,13 @@ +* +!.gitignore + +!base.yml +!build.yml +!files-local-logs.yml +!files-local-nomos-env.yml +!files-local-php-sessions.yml +!files-local.yml +!network-backend.yml +!network-bridge.yml +!network-proxy.yml +!watchtower-disable.yml diff --git a/docker-compose/backend/php/base.yml b/docker-compose/backend/php/base.yml new file mode 100644 index 00000000..fd8526a2 --- /dev/null +++ b/docker-compose/backend/php/base.yml @@ -0,0 +1,6 @@ +services: + backend-php: + image: vanhack/nomos-backend-php + restart: always + volumes: + - ${PWD}/data/backup:/var/www/html/backup diff --git a/docker-compose/backend/php/build.yml b/docker-compose/backend/php/build.yml new file mode 100644 index 00000000..bcdc4962 --- /dev/null +++ b/docker-compose/backend/php/build.yml @@ -0,0 +1,6 @@ +services: + backend-php: + build: + context: ${PWD} + dockerfile: docker-compose/Dockerfile + target: backend-php diff --git a/docker-compose/backend/php/files-local-logs.yml b/docker-compose/backend/php/files-local-logs.yml new file mode 100644 index 00000000..9bc1f08e --- /dev/null +++ b/docker-compose/backend/php/files-local-logs.yml @@ -0,0 +1,4 @@ +services: + backend-php: + volumes: + - ${PWD}/logs:/var/www/html/logs diff --git a/docker-compose/backend/php/files-local-nomos-env.yml b/docker-compose/backend/php/files-local-nomos-env.yml new file mode 100644 index 00000000..0da895f7 --- /dev/null +++ b/docker-compose/backend/php/files-local-nomos-env.yml @@ -0,0 +1,4 @@ +services: + backend-php: + env_file: + - ${PWD}/docker/nomos.env diff --git a/docker-compose/backend/php/files-local-php-sessions.yml b/docker-compose/backend/php/files-local-php-sessions.yml new file mode 100644 index 00000000..861b9da3 --- /dev/null +++ b/docker-compose/backend/php/files-local-php-sessions.yml @@ -0,0 +1,4 @@ +services: + backend-php: + volumes: + - ${PWD}/data/sessions:/sessions diff --git a/docker-compose/backend/php/files-local.yml b/docker-compose/backend/php/files-local.yml new file mode 100644 index 00000000..5f9e41a7 --- /dev/null +++ b/docker-compose/backend/php/files-local.yml @@ -0,0 +1,7 @@ +services: + backend-php: + volumes: + - ${PWD}/packages/backend-php/app:/var/www/html/app + - ${PWD}/migrations:/var/www/html/migrations + - ${PWD}/packages/backend-php/vhs:/var/www/html/vhs + - ${PWD}/logs:/var/www/html/logs diff --git a/docker-compose/backend/php/network-backend.yml b/docker-compose/backend/php/network-backend.yml new file mode 100644 index 00000000..5f8a3dd7 --- /dev/null +++ b/docker-compose/backend/php/network-backend.yml @@ -0,0 +1,4 @@ +services: + backend-php: + networks: + - backend diff --git a/docker-compose/backend/php/network-bridge.yml b/docker-compose/backend/php/network-bridge.yml new file mode 100644 index 00000000..bea99849 --- /dev/null +++ b/docker-compose/backend/php/network-bridge.yml @@ -0,0 +1,3 @@ +services: + backend-php: + network_mode: bridge diff --git a/docker-compose/backend/php/network-proxy.yml b/docker-compose/backend/php/network-proxy.yml new file mode 100644 index 00000000..0c79f829 --- /dev/null +++ b/docker-compose/backend/php/network-proxy.yml @@ -0,0 +1,4 @@ +services: + backend-php: + networks: + - proxy diff --git a/docker-compose/backend/php/watchtower-disable.yml b/docker-compose/backend/php/watchtower-disable.yml new file mode 100644 index 00000000..bbe1a645 --- /dev/null +++ b/docker-compose/backend/php/watchtower-disable.yml @@ -0,0 +1,4 @@ +services: + backend-php: + labels: + - 'com.centurylinklabs.watchtower.enable=false' diff --git a/docker-compose/build-backend.yml b/docker-compose/build-backend.yml deleted file mode 100644 index 4db5651b..00000000 --- a/docker-compose/build-backend.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-backend: - build: - context: .. - dockerfile: docker-compose/Dockerfile - target: backend diff --git a/docker-compose/build-frontend.yml b/docker-compose/build-frontend.yml deleted file mode 100644 index c133cca0..00000000 --- a/docker-compose/build-frontend.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-frontend: - build: - context: .. - dockerfile: docker-compose/Dockerfile - target: frontend diff --git a/docker-compose/common/.gitignore b/docker-compose/common/.gitignore new file mode 100644 index 00000000..ddaba61a --- /dev/null +++ b/docker-compose/common/.gitignore @@ -0,0 +1,7 @@ +* +!.gitignore + +!network-backend-internal.yml +!network-proxy-internal.yml +!network-proxy-external.yml +!watchtower-disable.yml diff --git a/docker-compose/common/network-backend-internal.yml b/docker-compose/common/network-backend-internal.yml new file mode 100644 index 00000000..3bf3eadc --- /dev/null +++ b/docker-compose/common/network-backend-internal.yml @@ -0,0 +1,2 @@ +networks: + backend: diff --git a/docker-compose/common/network-proxy-external.yml b/docker-compose/common/network-proxy-external.yml new file mode 100644 index 00000000..665b6d62 --- /dev/null +++ b/docker-compose/common/network-proxy-external.yml @@ -0,0 +1,3 @@ +networks: + proxy: + external: true diff --git a/docker-compose/common/network-proxy-internal.yml b/docker-compose/common/network-proxy-internal.yml new file mode 100644 index 00000000..06ebd527 --- /dev/null +++ b/docker-compose/common/network-proxy-internal.yml @@ -0,0 +1,2 @@ +networks: + proxy: diff --git a/docker-compose/core.network-bridge.yml b/docker-compose/core.network-bridge.yml deleted file mode 100644 index 62690360..00000000 --- a/docker-compose/core.network-bridge.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-frontend: - network_mode: bridge - - nomos-backend: - network_mode: bridge diff --git a/docker-compose/core.network-proxy-internal.yml b/docker-compose/core.network-proxy-internal.yml deleted file mode 100644 index 87c60875..00000000 --- a/docker-compose/core.network-proxy-internal.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - nomos-frontend: - networks: - - proxy - - nomos-backend: - networks: - - proxy - - nomos-webhooker: - networks: - - proxy - -networks: - proxy: diff --git a/docker-compose/core.network-proxy.yml b/docker-compose/core.network-proxy.yml deleted file mode 100644 index b09da5f6..00000000 --- a/docker-compose/core.network-proxy.yml +++ /dev/null @@ -1,12 +0,0 @@ -services: - nomos-frontend: - networks: - - proxy - - nomos-backend: - networks: - - proxy - -networks: - proxy: - external: true diff --git a/docker-compose/core.ports.yml b/docker-compose/core.ports.yml deleted file mode 100644 index e310c996..00000000 --- a/docker-compose/core.ports.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-frontend: - ports: - - 80:80 diff --git a/docker-compose/core.yml b/docker-compose/core.yml deleted file mode 100644 index b514ccd2..00000000 --- a/docker-compose/core.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - nomos-frontend: - image: vanhack/nomos-frontend - container_name: nomos-frontend - depends_on: - - nomos-backend - restart: always - links: - - nomos-backend:nomos-backend - - nomos-backend: - image: vanhack/nomos-backend - container_name: nomos-backend - restart: always diff --git a/docker-compose/files-local-backend.yml b/docker-compose/files-local-backend.yml deleted file mode 100644 index 79274506..00000000 --- a/docker-compose/files-local-backend.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - nomos-backend: - volumes: - - ../app:/var/www/html/app - - ../migrations:/var/www/html/migrations - - ../vhs:/var/www/html/vhs - - ../logs:/var/www/html/logs diff --git a/docker-compose/files-local-frontend.yml b/docker-compose/files-local-frontend.yml deleted file mode 100644 index fb73f7e3..00000000 --- a/docker-compose/files-local-frontend.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - nomos-frontend: - volumes: - - ../web:/var/www/html - - ../conf/nginx-vhost-docker-compose.conf:/etc/nginx/conf.d/default.conf diff --git a/docker-compose/files-local-nomos-env.yml b/docker-compose/files-local-nomos-env.yml deleted file mode 100644 index d145159a..00000000 --- a/docker-compose/files-local-nomos-env.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-backend: - env_file: - - ../docker/nomos.env diff --git a/docker-compose/frontend/.gitignore b/docker-compose/frontend/.gitignore new file mode 100644 index 00000000..efd6367c --- /dev/null +++ b/docker-compose/frontend/.gitignore @@ -0,0 +1,5 @@ +* +!.gitignore + +!react/ +!web/ diff --git a/docker-compose/frontend/react/.gitignore b/docker-compose/frontend/react/.gitignore new file mode 100644 index 00000000..566bba26 --- /dev/null +++ b/docker-compose/frontend/react/.gitignore @@ -0,0 +1,19 @@ +* +!.gitignore + +!base.yml +!build.yml +!files-local.yml +!network-backend.yml +!network-bridge.yml +!network-proxy.yml +!nginx-proxy-devtest.yml +!nginx-proxy-prod.yml +!ports.yml +!traefik-devtest-http-only.yml +!traefik-devtest-new-http-only.yml +!traefik-devtest-new.yml +!traefik-devtest.yml +!traefik-prod-new.yml +!traefik-prod.yml +!watchtower-disable.yml diff --git a/docker-compose/frontend/react/base.yml b/docker-compose/frontend/react/base.yml new file mode 100644 index 00000000..45d50e13 --- /dev/null +++ b/docker-compose/frontend/react/base.yml @@ -0,0 +1,8 @@ +services: + frontend-react: + image: vanhack/nomos-frontend-react + depends_on: + - backend-php + restart: always + links: + - backend-php:backend-php diff --git a/docker-compose/frontend/react/build.yml b/docker-compose/frontend/react/build.yml new file mode 100644 index 00000000..e0ac74dd --- /dev/null +++ b/docker-compose/frontend/react/build.yml @@ -0,0 +1,6 @@ +services: + frontend-react: + build: + context: ${PWD} + dockerfile: docker-compose/Dockerfile + target: frontend-react diff --git a/docker-compose/frontend/react/files-local.yml b/docker-compose/frontend/react/files-local.yml new file mode 100644 index 00000000..3e75bd82 --- /dev/null +++ b/docker-compose/frontend/react/files-local.yml @@ -0,0 +1,5 @@ +services: + frontend-react: + volumes: + - ${PWD}/packages/frontend-react/dist:/var/www/html + - ${PWD}/conf/nginx-react-docker-compose.conf:/etc/nginx/conf.d/default.conf diff --git a/docker-compose/frontend/react/network-backend.yml b/docker-compose/frontend/react/network-backend.yml new file mode 100644 index 00000000..e00ad069 --- /dev/null +++ b/docker-compose/frontend/react/network-backend.yml @@ -0,0 +1,4 @@ +services: + frontend-react: + networks: + - backend diff --git a/docker-compose/frontend/react/network-bridge.yml b/docker-compose/frontend/react/network-bridge.yml new file mode 100644 index 00000000..96a436ea --- /dev/null +++ b/docker-compose/frontend/react/network-bridge.yml @@ -0,0 +1,3 @@ +services: + frontend-react: + network_mode: bridge diff --git a/docker-compose/frontend/react/network-proxy.yml b/docker-compose/frontend/react/network-proxy.yml new file mode 100644 index 00000000..13389391 --- /dev/null +++ b/docker-compose/frontend/react/network-proxy.yml @@ -0,0 +1,4 @@ +services: + frontend-react: + networks: + - proxy diff --git a/docker-compose/frontend/react/nginx-proxy-devtest.yml b/docker-compose/frontend/react/nginx-proxy-devtest.yml new file mode 100644 index 00000000..a715a594 --- /dev/null +++ b/docker-compose/frontend/react/nginx-proxy-devtest.yml @@ -0,0 +1,5 @@ +services: + frontend-react: + environment: + VIRTUAL_HOST: membership.test.vanhack.ca,membership.devtest.vanhack.ca + LETSENCRYPT_HOST: membership.test.vanhack.ca,membership.devtest.vanhack.ca diff --git a/docker-compose/frontend/react/nginx-proxy-prod.yml b/docker-compose/frontend/react/nginx-proxy-prod.yml new file mode 100644 index 00000000..e7946c54 --- /dev/null +++ b/docker-compose/frontend/react/nginx-proxy-prod.yml @@ -0,0 +1,5 @@ +services: + frontend-react: + environment: + VIRTUAL_HOST: membership.vanhack.ca + LETSENCRYPT_HOST: membership.vanhack.ca diff --git a/docker-compose/frontend/react/ports.yml b/docker-compose/frontend/react/ports.yml new file mode 100644 index 00000000..d2d06b7f --- /dev/null +++ b/docker-compose/frontend/react/ports.yml @@ -0,0 +1,4 @@ +services: + frontend-react: + ports: + - 80:80 diff --git a/docker-compose/frontend/react/traefik-devtest-http-only.yml b/docker-compose/frontend/react/traefik-devtest-http-only.yml new file mode 100644 index 00000000..618c1d7a --- /dev/null +++ b/docker-compose/frontend/react/traefik-devtest-http-only.yml @@ -0,0 +1,11 @@ +services: + frontend-react: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.server.scheme=http' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest.rule=Host(`membership.test.vanhack.ca`) || Host(`membership.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devtest.entryPoints=web' + - 'traefik.http.routers.nomos-devtest.service=svc-nomos-devtest' diff --git a/docker-compose/frontend/react/traefik-devtest-new-http-only.yml b/docker-compose/frontend/react/traefik-devtest-new-http-only.yml new file mode 100644 index 00000000..0aeaf214 --- /dev/null +++ b/docker-compose/frontend/react/traefik-devtest-new-http-only.yml @@ -0,0 +1,11 @@ +services: + frontend-react: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest-new.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest-new.loadbalancer.server.scheme=http' + - 'traefik.http.services.svc-nomos-devtest-new.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest-new.rule=Host(`membership-new.test.vanhack.ca`) || Host(`membership-new.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devtest-new.entryPoints=web' + - 'traefik.http.routers.nomos-devtest-new.service=svc-nomos-devtest-new' diff --git a/docker-compose/frontend/react/traefik-devtest-new.yml b/docker-compose/frontend/react/traefik-devtest-new.yml new file mode 100644 index 00000000..ff1e276c --- /dev/null +++ b/docker-compose/frontend/react/traefik-devtest-new.yml @@ -0,0 +1,12 @@ +services: + frontend-react: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest-new.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest-new.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest-new.rule=Host(`membership-new.test.vanhack.ca`) || Host(`membership-new.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devdevtest-newtest.tls=true' + - 'traefik.http.routers.nomos-devtest-new.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-devtest-new.entryPoints=websecure' + - 'traefik.http.routers.nomos-devtest-new.service=svc-nomos-devtest-new' diff --git a/docker-compose/frontend/react/traefik-devtest.yml b/docker-compose/frontend/react/traefik-devtest.yml new file mode 100644 index 00000000..b4b7112c --- /dev/null +++ b/docker-compose/frontend/react/traefik-devtest.yml @@ -0,0 +1,12 @@ +services: + frontend-react: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest.rule=Host(`membership.test.vanhack.ca`) || Host(`membership.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devtest.tls=true' + - 'traefik.http.routers.nomos-devtest.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-devtest.entryPoints=websecure' + - 'traefik.http.routers.nomos-devtest.service=svc-nomos-devtest' diff --git a/docker-compose/frontend/react/traefik-prod-new.yml b/docker-compose/frontend/react/traefik-prod-new.yml new file mode 100644 index 00000000..91d7e6f7 --- /dev/null +++ b/docker-compose/frontend/react/traefik-prod-new.yml @@ -0,0 +1,12 @@ +services: + frontend-react: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-prod-new.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-prod-new.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-prod-new.rule=Host(`membership-new.vanhack.ca`)' + - 'traefik.http.routers.nomos-prod-new.tls=true' + - 'traefik.http.routers.nomos-prod-new.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-prod-new.entryPoints=websecure' + - 'traefik.http.routers.nomos-prod-new.service=svc-nomos-prod-new' diff --git a/docker-compose/frontend/react/traefik-prod.yml b/docker-compose/frontend/react/traefik-prod.yml new file mode 100644 index 00000000..6e531da5 --- /dev/null +++ b/docker-compose/frontend/react/traefik-prod.yml @@ -0,0 +1,12 @@ +services: + frontend-react: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-prod.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-prod.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-prod.rule=Host(`membership.vanhack.ca`)' + - 'traefik.http.routers.nomos-prod.tls=true' + - 'traefik.http.routers.nomos-prod.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-prod.entryPoints=websecure' + - 'traefik.http.routers.nomos-prod.service=svc-nomos-prod' diff --git a/docker-compose/frontend/react/watchtower-disable.yml b/docker-compose/frontend/react/watchtower-disable.yml new file mode 100644 index 00000000..fc07bd23 --- /dev/null +++ b/docker-compose/frontend/react/watchtower-disable.yml @@ -0,0 +1,4 @@ +services: + frontend-react: + labels: + - 'com.centurylinklabs.watchtower.enable=false' diff --git a/docker-compose/frontend/web/.gitignore b/docker-compose/frontend/web/.gitignore new file mode 100644 index 00000000..c32a9b4e --- /dev/null +++ b/docker-compose/frontend/web/.gitignore @@ -0,0 +1,20 @@ +* +!.gitignore + +!base.yml +!build.yml +!files-local.yml +!image-web.yml +!network-backend.yml +!network-bridge.yml +!network-proxy.yml +!nginx-proxy-devtest.yml +!nginx-proxy-prod.yml +!ports.yml +!traefik-devtest-http-only.yml +!traefik-devtest-old-http-only.yml +!traefik-devtest-old.yml +!traefik-devtest.yml +!traefik-prod-old.yml +!traefik-prod.yml +!watchtower-disable.yml diff --git a/docker-compose/frontend/web/base.yml b/docker-compose/frontend/web/base.yml new file mode 100644 index 00000000..370f0bd1 --- /dev/null +++ b/docker-compose/frontend/web/base.yml @@ -0,0 +1,8 @@ +services: + frontend-web: + image: vanhack/nomos-frontend-web + depends_on: + - backend-php + restart: always + links: + - backend-php:backend-php diff --git a/docker-compose/frontend/web/build.yml b/docker-compose/frontend/web/build.yml new file mode 100644 index 00000000..6778add6 --- /dev/null +++ b/docker-compose/frontend/web/build.yml @@ -0,0 +1,6 @@ +services: + frontend-web: + build: + context: ${PWD}/ + dockerfile: docker-compose/Dockerfile + target: frontend-web diff --git a/docker-compose/frontend/web/files-local.yml b/docker-compose/frontend/web/files-local.yml new file mode 100644 index 00000000..40ffed03 --- /dev/null +++ b/docker-compose/frontend/web/files-local.yml @@ -0,0 +1,5 @@ +services: + frontend-web: + volumes: + - ${PWD}/packages/frontend-web/web:/var/www/html + - ${PWD}/conf/nginx-vhost-docker-compose.conf:/etc/nginx/conf.d/default.conf diff --git a/docker-compose/frontend/web/network-backend.yml b/docker-compose/frontend/web/network-backend.yml new file mode 100644 index 00000000..222cac41 --- /dev/null +++ b/docker-compose/frontend/web/network-backend.yml @@ -0,0 +1,4 @@ +services: + frontend-web: + networks: + - backend diff --git a/docker-compose/frontend/web/network-bridge.yml b/docker-compose/frontend/web/network-bridge.yml new file mode 100644 index 00000000..efe44a07 --- /dev/null +++ b/docker-compose/frontend/web/network-bridge.yml @@ -0,0 +1,3 @@ +services: + frontend-web: + network_mode: bridge diff --git a/docker-compose/frontend/web/network-proxy.yml b/docker-compose/frontend/web/network-proxy.yml new file mode 100644 index 00000000..35a4d515 --- /dev/null +++ b/docker-compose/frontend/web/network-proxy.yml @@ -0,0 +1,4 @@ +services: + frontend-web: + networks: + - proxy diff --git a/docker-compose/frontend/web/nginx-proxy-devtest.yml b/docker-compose/frontend/web/nginx-proxy-devtest.yml new file mode 100644 index 00000000..e1f63f3d --- /dev/null +++ b/docker-compose/frontend/web/nginx-proxy-devtest.yml @@ -0,0 +1,5 @@ +services: + frontend-web: + environment: + VIRTUAL_HOST: membership.test.vanhack.ca,membership.devtest.vanhack.ca + LETSENCRYPT_HOST: membership.test.vanhack.ca,membership.devtest.vanhack.ca diff --git a/docker-compose/frontend/web/nginx-proxy-prod.yml b/docker-compose/frontend/web/nginx-proxy-prod.yml new file mode 100644 index 00000000..262420db --- /dev/null +++ b/docker-compose/frontend/web/nginx-proxy-prod.yml @@ -0,0 +1,5 @@ +services: + frontend-web: + environment: + VIRTUAL_HOST: membership.vanhack.ca + LETSENCRYPT_HOST: membership.vanhack.ca diff --git a/docker-compose/frontend/web/ports.yml b/docker-compose/frontend/web/ports.yml new file mode 100644 index 00000000..e33f7055 --- /dev/null +++ b/docker-compose/frontend/web/ports.yml @@ -0,0 +1,4 @@ +services: + frontend-web: + ports: + - 80:80 diff --git a/docker-compose/frontend/web/traefik-devtest-http-only.yml b/docker-compose/frontend/web/traefik-devtest-http-only.yml new file mode 100644 index 00000000..f88aefb4 --- /dev/null +++ b/docker-compose/frontend/web/traefik-devtest-http-only.yml @@ -0,0 +1,11 @@ +services: + frontend-web: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.server.scheme=http' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest.rule=Host(`membership.test.vanhack.ca`) || Host(`membership.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devtest.entryPoints=web' + - 'traefik.http.routers.nomos-devtest.service=svc-nomos-devtest' diff --git a/docker-compose/frontend/web/traefik-devtest-old-http-only.yml b/docker-compose/frontend/web/traefik-devtest-old-http-only.yml new file mode 100644 index 00000000..09f3f954 --- /dev/null +++ b/docker-compose/frontend/web/traefik-devtest-old-http-only.yml @@ -0,0 +1,11 @@ +services: + frontend-web: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest-old.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest-old.loadbalancer.server.scheme=http' + - 'traefik.http.services.svc-nomos-devtest-old.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest-old.rule=Host(`membership-old.test.vanhack.ca`) || Host(`membership-old.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devtest-old.entryPoints=web' + - 'traefik.http.routers.nomos-devtest-old.service=svc-nomos-devtest-old' diff --git a/docker-compose/frontend/web/traefik-devtest-old.yml b/docker-compose/frontend/web/traefik-devtest-old.yml new file mode 100644 index 00000000..bf228c51 --- /dev/null +++ b/docker-compose/frontend/web/traefik-devtest-old.yml @@ -0,0 +1,12 @@ +services: + frontend-web: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest-old.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest-old.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest-old.rule=Host(`membership-old.test.vanhack.ca`) || Host(`membership-old.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devtest-old.tls=true' + - 'traefik.http.routers.nomos-devtest-old.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-devtest-old.entryPoints=websecure' + - 'traefik.http.routers.nomos-devtest-old.service=svc-nomos-devtest-old' diff --git a/docker-compose/frontend/web/traefik-devtest.yml b/docker-compose/frontend/web/traefik-devtest.yml new file mode 100644 index 00000000..9a90db07 --- /dev/null +++ b/docker-compose/frontend/web/traefik-devtest.yml @@ -0,0 +1,12 @@ +services: + frontend-web: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-devtest.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-devtest.rule=Host(`membership.test.vanhack.ca`) || Host(`membership.devtest.vanhack.ca`)' + - 'traefik.http.routers.nomos-devtest.tls=true' + - 'traefik.http.routers.nomos-devtest.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-devtest.entryPoints=websecure' + - 'traefik.http.routers.nomos-devtest.service=svc-nomos-devtest' diff --git a/docker-compose/frontend/web/traefik-prod-old.yml b/docker-compose/frontend/web/traefik-prod-old.yml new file mode 100644 index 00000000..36c0eac3 --- /dev/null +++ b/docker-compose/frontend/web/traefik-prod-old.yml @@ -0,0 +1,12 @@ +services: + frontend-web: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-prod-old.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-prod-old.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-prod-old.rule=Host(`membership-old.vanhack.ca`)' + - 'traefik.http.routers.nomos-prod-old.tls=true' + - 'traefik.http.routers.nomos-prod-old.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-prod-old.entryPoints=websecure' + - 'traefik.http.routers.nomos-prod-old.service=svc-nomos-prod-old' diff --git a/docker-compose/frontend/web/traefik-prod.yml b/docker-compose/frontend/web/traefik-prod.yml new file mode 100644 index 00000000..75efedc7 --- /dev/null +++ b/docker-compose/frontend/web/traefik-prod.yml @@ -0,0 +1,12 @@ +services: + frontend-web: + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=proxy' + - 'traefik.http.services.svc-nomos-prod.loadbalancer.server.port=80' + - 'traefik.http.services.svc-nomos-prod.loadbalancer.sticky.cookie=true' + - 'traefik.http.routers.nomos-prod.rule=Host(`membership.vanhack.ca`)' + - 'traefik.http.routers.nomos-prod.tls=true' + - 'traefik.http.routers.nomos-prod.tls.certresolver=lets-encrypt' + - 'traefik.http.routers.nomos-prod.entryPoints=websecure' + - 'traefik.http.routers.nomos-prod.service=svc-nomos-prod' diff --git a/docker-compose/frontend/web/watchtower-disable.yml b/docker-compose/frontend/web/watchtower-disable.yml new file mode 100644 index 00000000..c5737f7b --- /dev/null +++ b/docker-compose/frontend/web/watchtower-disable.yml @@ -0,0 +1,4 @@ +services: + frontend-web: + labels: + - 'com.centurylinklabs.watchtower.enable=false' diff --git a/docker-compose/mysql-external-mysqld.yml b/docker-compose/mysql-external-mysqld.yml deleted file mode 100644 index c9872b61..00000000 --- a/docker-compose/mysql-external-mysqld.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-backend: - external_links: - - mysqld:mysqld diff --git a/docker-compose/mysql-external-network-mysql-lithium.yml b/docker-compose/mysql-external-network-mysql-lithium.yml deleted file mode 100644 index 532b9828..00000000 --- a/docker-compose/mysql-external-network-mysql-lithium.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - nomos-backend: - networks: - - mysql - -networks: - mysql: - external: true - name: mysql-lithium diff --git a/docker-compose/mysql-external-network-mysql.yml b/docker-compose/mysql-external-network-mysql.yml deleted file mode 100644 index 232ff5da..00000000 --- a/docker-compose/mysql-external-network-mysql.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - nomos-backend: - networks: - - mysql - -networks: - mysql: - external: true diff --git a/docker-compose/mysql-external-variable.yml b/docker-compose/mysql-external-variable.yml deleted file mode 100644 index 9383a0d1..00000000 --- a/docker-compose/mysql-external-variable.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-backend: - external_links: - - ${EXTERNAL_MYSQL_HOST}:nomos-mysql diff --git a/docker-compose/mysql-local-network-bridge.yml b/docker-compose/mysql-local-network-bridge.yml deleted file mode 100644 index 955ca9f6..00000000 --- a/docker-compose/mysql-local-network-bridge.yml +++ /dev/null @@ -1,3 +0,0 @@ -services: - nomos-mysql: - network_mode: bridge diff --git a/docker-compose/mysql-local-network-mysql.yml b/docker-compose/mysql-local-network-mysql.yml deleted file mode 100644 index a93e5a36..00000000 --- a/docker-compose/mysql-local-network-mysql.yml +++ /dev/null @@ -1,11 +0,0 @@ -services: - nomos-backend: - networks: - - mysql - - nomos-mysql: - networks: - - mysql - -networks: - mysql: diff --git a/docker-compose/mysql-local.yml b/docker-compose/mysql-local.yml deleted file mode 100644 index 4ba705b3..00000000 --- a/docker-compose/mysql-local.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - # backend needs the db started first - nomos-backend: - depends_on: - nomos-mysql: - condition: service_started - nomos-mysql: - image: mysql:5.7 - container_name: nomos-mysql - environment: - - MYSQL_USER=${NOMOS_DB_USER} - - MYSQL_PASSWORD=${NOMOS_DB_PASSWORD} - - MYSQL_DATABASE=${NOMOS_DB_DATABASE} - - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - volumes: - - ../data/mysql:/var/lib/mysql - # Workaround this bug in the mysql image: https://github.com/docker-library/mysql/issues/579 - ulimits: - nproc: 65535 - nofile: - soft: 20000 - hard: 40000 diff --git a/docker-compose/mysql/.gitignore b/docker-compose/mysql/.gitignore new file mode 100644 index 00000000..eb386c9b --- /dev/null +++ b/docker-compose/mysql/.gitignore @@ -0,0 +1,10 @@ +* +!.gitignore + +!external-mysqld.yml +!external-network-lithium.yml +!external-network-mysql.yml +!external-variable.yml +!local-network-bridge.yml +!local-network-mysql.yml +!local.yml diff --git a/docker-compose/mysql/external-mysqld.yml b/docker-compose/mysql/external-mysqld.yml new file mode 100644 index 00000000..dd2a0ee2 --- /dev/null +++ b/docker-compose/mysql/external-mysqld.yml @@ -0,0 +1,4 @@ +services: + backend-php: + external_links: + - mysqld:mysqld diff --git a/docker-compose/mysql/external-network-mysql.yml b/docker-compose/mysql/external-network-mysql.yml new file mode 100644 index 00000000..7b5e1cdf --- /dev/null +++ b/docker-compose/mysql/external-network-mysql.yml @@ -0,0 +1,8 @@ +services: + backend-php: + networks: + - mysql + +networks: + mysql: + external: true diff --git a/docker-compose/mysql/external-variable.yml b/docker-compose/mysql/external-variable.yml new file mode 100644 index 00000000..c6c4b56a --- /dev/null +++ b/docker-compose/mysql/external-variable.yml @@ -0,0 +1,4 @@ +services: + backend-php: + external_links: + - ${EXTERNAL_MYSQL_HOST}:mysql diff --git a/docker-compose/mysql/local-network-bridge.yml b/docker-compose/mysql/local-network-bridge.yml new file mode 100644 index 00000000..78299f36 --- /dev/null +++ b/docker-compose/mysql/local-network-bridge.yml @@ -0,0 +1,3 @@ +services: + mysql: + network_mode: bridge diff --git a/docker-compose/mysql/local-network-mysql.yml b/docker-compose/mysql/local-network-mysql.yml new file mode 100644 index 00000000..d99faa31 --- /dev/null +++ b/docker-compose/mysql/local-network-mysql.yml @@ -0,0 +1,11 @@ +services: + backend-php: + networks: + - mysql + + mysql: + networks: + - mysql + +networks: + mysql: diff --git a/docker-compose/mysql/local.yml b/docker-compose/mysql/local.yml new file mode 100644 index 00000000..9c0c9d71 --- /dev/null +++ b/docker-compose/mysql/local.yml @@ -0,0 +1,21 @@ +services: + # backend needs the db started first + backend-php: + depends_on: + mysql: + condition: service_started + mysql: + image: mysql:8.0 + environment: + - MYSQL_USER=${NOMOS_DB_USER} + - MYSQL_PASSWORD=${NOMOS_DB_PASSWORD} + - MYSQL_DATABASE=${NOMOS_DB_DATABASE} + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + volumes: + - ${PWD}/data/mysql:/var/lib/mysql + # Workaround this bug in the mysql image: https://github.com/docker-library/mysql/issues/579 + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 diff --git a/docker-compose/nginx-proxy-devtest.yml b/docker-compose/nginx-proxy-devtest.yml deleted file mode 100644 index 1d76d0e7..00000000 --- a/docker-compose/nginx-proxy-devtest.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - nomos-frontend: - environment: - VIRTUAL_HOST: membership.test.vanhack.ca - LETSENCRYPT_HOST: membership.test.vanhack.ca diff --git a/docker-compose/nginx-proxy-prod.yml b/docker-compose/nginx-proxy-prod.yml deleted file mode 100644 index e4fa0cbb..00000000 --- a/docker-compose/nginx-proxy-prod.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - nomos-frontend: - environment: - VIRTUAL_HOST: membership.vanhack.ca - LETSENCRYPT_HOST: membership.vanhack.ca diff --git a/docker-compose/override.sample.yml b/docker-compose/override.sample.yml new file mode 100644 index 00000000..41715207 --- /dev/null +++ b/docker-compose/override.sample.yml @@ -0,0 +1,16 @@ +services: + rabbitmq: + labels: + - 'com.centurylinklabs.watchtower.enable=false' + webhooker: + labels: + - 'com.centurylinklabs.watchtower.enable=false' + backend-php: + labels: + - 'com.centurylinklabs.watchtower.enable=false' + frontend-react: + labels: + - 'com.centurylinklabs.watchtower.enable=false' + frontend-web: + labels: + - 'com.centurylinklabs.watchtower.enable=false' diff --git a/docker-compose/override.yml b/docker-compose/override.yml deleted file mode 100644 index 5803ce7c..00000000 --- a/docker-compose/override.yml +++ /dev/null @@ -1,13 +0,0 @@ -services: - nomos-rabbitmq: - labels: - - 'com.centurylinklabs.watchtower.enable=false' - nomos-webhooker: - labels: - - 'com.centurylinklabs.watchtower.enable=false' - nomos-backend: - labels: - - 'com.centurylinklabs.watchtower.enable=false' - nomos-frontend: - labels: - - 'com.centurylinklabs.watchtower.enable=false' diff --git a/docker-compose/rabbitmq-external.yml b/docker-compose/rabbitmq-external.yml deleted file mode 100644 index 28215ef7..00000000 --- a/docker-compose/rabbitmq-external.yml +++ /dev/null @@ -1,16 +0,0 @@ -services: - nomos-webhooker: - external_links: - - rabbitmq:nomos-rabbitmq - networks: - - rabbitmq - - nomos-backend: - external_links: - - rabbitmq:nomos-rabbitmq - networks: - - rabbitmq - -networks: - rabbitmq: - external: true diff --git a/docker-compose/rabbitmq-local-management.yml b/docker-compose/rabbitmq-local-management.yml deleted file mode 100644 index 64242d04..00000000 --- a/docker-compose/rabbitmq-local-management.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-rabbitmq: - ports: - - 15672:15672 diff --git a/docker-compose/rabbitmq-local-network-bridge.yml b/docker-compose/rabbitmq-local-network-bridge.yml deleted file mode 100644 index 50a4c7d4..00000000 --- a/docker-compose/rabbitmq-local-network-bridge.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - nomos-rabbitmq: - network_mode: bridge - - nomos-webhooker: - network_mode: bridge diff --git a/docker-compose/rabbitmq-local-network-internal.yml b/docker-compose/rabbitmq-local-network-internal.yml deleted file mode 100644 index 59224803..00000000 --- a/docker-compose/rabbitmq-local-network-internal.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - nomos-rabbitmq: - networks: - - rabbitmq - - nomos-webhooker: - networks: - - rabbitmq - - nomos-backend: - networks: - - rabbitmq - -networks: - rabbitmq: diff --git a/docker-compose/rabbitmq-local-network-rabbitmq.yml b/docker-compose/rabbitmq-local-network-rabbitmq.yml deleted file mode 100644 index 66a6012a..00000000 --- a/docker-compose/rabbitmq-local-network-rabbitmq.yml +++ /dev/null @@ -1,16 +0,0 @@ -services: - nomos-rabbitmq: - networks: - - rabbitmq - - nomos-webhooker: - networks: - - rabbitmq - - nomos-backend: - networks: - - rabbitmq - -networks: - rabbitmq: - external: true diff --git a/docker-compose/rabbitmq-local.yml b/docker-compose/rabbitmq-local.yml deleted file mode 100644 index 2e10d434..00000000 --- a/docker-compose/rabbitmq-local.yml +++ /dev/null @@ -1,21 +0,0 @@ -services: - nomos-rabbitmq: - image: rabbitmq:3-management - container_name: nomos-rabbitmq - restart: always - env_file: - - ../docker/nomos.env - environment: - - RABBITMQ_DEFAULT_USER=${NOMOS_RABBITMQ_USER} - - RABBITMQ_DEFAULT_PASS=${NOMOS_RABBITMQ_PASSWORD} - - RABBITMQ_DEFAULT_VHOST=${NOMOS_RABBITMQ_VHOST} - volumes: - - ../data/rabbitmq:/var/lib/rabbitmq - - nomos-webhooker: - depends_on: - - nomos-rabbitmq - - nomos-backend: - depends_on: - - nomos-rabbitmq diff --git a/docker-compose/rabbitmq/.gitignore b/docker-compose/rabbitmq/.gitignore new file mode 100644 index 00000000..293acf26 --- /dev/null +++ b/docker-compose/rabbitmq/.gitignore @@ -0,0 +1,9 @@ +* +!.gitignore + +!external.yml +!local-management.yml +!local-network-bridge.yml +!local-network-internal.yml +!local-network-rabbitmq.yml +!local.yml diff --git a/docker-compose/rabbitmq/external.yml b/docker-compose/rabbitmq/external.yml new file mode 100644 index 00000000..5d8975be --- /dev/null +++ b/docker-compose/rabbitmq/external.yml @@ -0,0 +1,16 @@ +services: + webhooker: + external_links: + - rabbitmq:rabbitmq + networks: + - rabbitmq + + backend-php: + external_links: + - rabbitmq:rabbitmq + networks: + - rabbitmq + +networks: + rabbitmq: + external: true diff --git a/docker-compose/rabbitmq/local-management.yml b/docker-compose/rabbitmq/local-management.yml new file mode 100644 index 00000000..2197dae3 --- /dev/null +++ b/docker-compose/rabbitmq/local-management.yml @@ -0,0 +1,4 @@ +services: + rabbitmq: + ports: + - 15672:15672 diff --git a/docker-compose/rabbitmq/local-network-bridge.yml b/docker-compose/rabbitmq/local-network-bridge.yml new file mode 100644 index 00000000..94c71c86 --- /dev/null +++ b/docker-compose/rabbitmq/local-network-bridge.yml @@ -0,0 +1,6 @@ +services: + rabbitmq: + network_mode: bridge + + webhooker: + network_mode: bridge diff --git a/docker-compose/rabbitmq/local-network-internal.yml b/docker-compose/rabbitmq/local-network-internal.yml new file mode 100644 index 00000000..d2bf80af --- /dev/null +++ b/docker-compose/rabbitmq/local-network-internal.yml @@ -0,0 +1,15 @@ +services: + rabbitmq: + networks: + - rabbitmq + + webhooker: + networks: + - rabbitmq + + backend-php: + networks: + - rabbitmq + +networks: + rabbitmq: diff --git a/docker-compose/rabbitmq/local-network-rabbitmq.yml b/docker-compose/rabbitmq/local-network-rabbitmq.yml new file mode 100644 index 00000000..b7402ada --- /dev/null +++ b/docker-compose/rabbitmq/local-network-rabbitmq.yml @@ -0,0 +1,16 @@ +services: + rabbitmq: + networks: + - rabbitmq + + webhooker: + networks: + - rabbitmq + + backend-php: + networks: + - rabbitmq + +networks: + rabbitmq: + external: true diff --git a/docker-compose/rabbitmq/local.yml b/docker-compose/rabbitmq/local.yml new file mode 100644 index 00000000..806c9e04 --- /dev/null +++ b/docker-compose/rabbitmq/local.yml @@ -0,0 +1,21 @@ +services: + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + restart: always + env_file: + - ${PWD}/docker/nomos.env + environment: + - RABBITMQ_DEFAULT_USER=${NOMOS_RABBITMQ_USER} + - RABBITMQ_DEFAULT_PASS=${NOMOS_RABBITMQ_PASSWORD} + - RABBITMQ_DEFAULT_VHOST=${NOMOS_RABBITMQ_VHOST} + volumes: + - ${PWD}/data/rabbitmq:/var/lib/rabbitmq + + webhooker: + depends_on: + - rabbitmq + + backend-php: + depends_on: + - rabbitmq diff --git a/docker-compose/traefik-devtest.yml b/docker-compose/traefik-devtest.yml deleted file mode 100644 index 1deb85ec..00000000 --- a/docker-compose/traefik-devtest.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - nomos-frontend: - labels: - - 'traefik.enable=true' - - 'traefik.http.routers.nomos-devtest.rule=Host(`membership.test.vanhack.ca`) || Host(`membership.devtest.vanhack.ca`)' - - 'traefik.http.routers.nomos-devtest.entryPoints=websecure' - - 'traefik.http.routers.nomos-devtest.tls=true' - - 'traefik.http.routers.nomos-devtest.tls.certresolver=lets-encrypt' - - 'traefik.port=80' - - 'traefik.docker.network=proxy' diff --git a/docker-compose/traefik-prod.yml b/docker-compose/traefik-prod.yml deleted file mode 100644 index 24e80017..00000000 --- a/docker-compose/traefik-prod.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - nomos-frontend: - labels: - - 'traefik.enable=true' - - 'traefik.http.routers.nomos-prod.rule=Host(`membership.vanhack.ca`)' - - 'traefik.http.routers.nomos-prod.entryPoints=websecure' - - 'traefik.http.routers.nomos-prod.tls=true' - - 'traefik.http.routers.nomos-prod.tls.certresolver=lets-encrypt' - - 'traefik.port=80' - - 'traefik.docker.network=proxy' diff --git a/docker-compose/webhooker-build.yml b/docker-compose/webhooker-build.yml deleted file mode 100644 index 0d9c5687..00000000 --- a/docker-compose/webhooker-build.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - nomos-webhooker: - build: - context: .. - dockerfile: docker-compose/Dockerfile - target: webhooker - env_file: ../docker/nomos.env diff --git a/docker-compose/webhooker-logs-local.yml b/docker-compose/webhooker-logs-local.yml deleted file mode 100644 index 7ed10d68..00000000 --- a/docker-compose/webhooker-logs-local.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - nomos-webhooker: - volumes: - - ../logs:/app/logs diff --git a/docker-compose/webhooker-network-bridge.yml b/docker-compose/webhooker-network-bridge.yml deleted file mode 100644 index f73b7550..00000000 --- a/docker-compose/webhooker-network-bridge.yml +++ /dev/null @@ -1,3 +0,0 @@ -services: - nomos-webhooker: - network_mode: bridge diff --git a/docker-compose/webhooker-network-proxy.yml b/docker-compose/webhooker-network-proxy.yml deleted file mode 100644 index 4958ce78..00000000 --- a/docker-compose/webhooker-network-proxy.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - nomos-webhooker: - networks: - - proxy - -networks: - proxy: - external: true diff --git a/docker-compose/webhooker-network-rabbitmq.yml b/docker-compose/webhooker-network-rabbitmq.yml deleted file mode 100644 index a2ed7ab2..00000000 --- a/docker-compose/webhooker-network-rabbitmq.yml +++ /dev/null @@ -1,12 +0,0 @@ -services: - nomos-frontend: - networks: - - rabbitmq - - nomos-webhooker: - networks: - - rabbitmq - -networks: - rabbitmq: - external: true diff --git a/docker-compose/webhooker.yml b/docker-compose/webhooker.yml deleted file mode 100644 index 5515d6ff..00000000 --- a/docker-compose/webhooker.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - nomos-webhooker: - image: vanhack/nomos-webhooker - container_name: nomos-webhooker - restart: always - - nomos-backend: - depends_on: - - nomos-webhooker diff --git a/docker-compose/webhooker/.gitignore b/docker-compose/webhooker/.gitignore new file mode 100644 index 00000000..5ede7818 --- /dev/null +++ b/docker-compose/webhooker/.gitignore @@ -0,0 +1,11 @@ +* +!.gitignore + +!base.yml +!build.yml +!logs-local.yml +!network-backend.yml +!network-bridge.yml +!network-proxy-internal.yml +!network-proxy.yml +!network-rabbitmq.yml diff --git a/docker-compose/webhooker/base.yml b/docker-compose/webhooker/base.yml new file mode 100644 index 00000000..2fdd84d4 --- /dev/null +++ b/docker-compose/webhooker/base.yml @@ -0,0 +1,9 @@ +services: + webhooker: + image: vanhack/webhooker + container_name: webhooker + restart: always + + backend-php: + depends_on: + - webhooker diff --git a/docker-compose/webhooker/build.yml b/docker-compose/webhooker/build.yml new file mode 100644 index 00000000..6b716d86 --- /dev/null +++ b/docker-compose/webhooker/build.yml @@ -0,0 +1,7 @@ +services: + webhooker: + build: + context: ${PWD} + dockerfile: docker-compose/Dockerfile + target: webhooker + env_file: ${PWD}/docker/nomos.env diff --git a/docker-compose/webhooker/logs-local.yml b/docker-compose/webhooker/logs-local.yml new file mode 100644 index 00000000..99c932ad --- /dev/null +++ b/docker-compose/webhooker/logs-local.yml @@ -0,0 +1,4 @@ +services: + webhooker: + volumes: + - ${PWD}/logs:/app/logs diff --git a/docker-compose/webhooker/network-backend.yml b/docker-compose/webhooker/network-backend.yml new file mode 100644 index 00000000..44b09726 --- /dev/null +++ b/docker-compose/webhooker/network-backend.yml @@ -0,0 +1,4 @@ +services: + webhooker: + networks: + - backend diff --git a/docker-compose/webhooker/network-bridge.yml b/docker-compose/webhooker/network-bridge.yml new file mode 100644 index 00000000..7dd3b885 --- /dev/null +++ b/docker-compose/webhooker/network-bridge.yml @@ -0,0 +1,3 @@ +services: + webhooker: + network_mode: bridge diff --git a/docker-compose/webhooker/network-proxy-internal.yml b/docker-compose/webhooker/network-proxy-internal.yml new file mode 100644 index 00000000..86ce6e2d --- /dev/null +++ b/docker-compose/webhooker/network-proxy-internal.yml @@ -0,0 +1,7 @@ +services: + webhooker: + networks: + - proxy + +networks: + proxy: diff --git a/docker-compose/webhooker/network-proxy.yml b/docker-compose/webhooker/network-proxy.yml new file mode 100644 index 00000000..0f7d3bb9 --- /dev/null +++ b/docker-compose/webhooker/network-proxy.yml @@ -0,0 +1,8 @@ +services: + webhooker: + networks: + - proxy + +networks: + proxy: + external: true diff --git a/docker-compose/webhooker/network-rabbitmq.yml b/docker-compose/webhooker/network-rabbitmq.yml new file mode 100644 index 00000000..c8a3e71b --- /dev/null +++ b/docker-compose/webhooker/network-rabbitmq.yml @@ -0,0 +1,12 @@ +services: + backend-php: + networks: + - rabbitmq + + webhooker: + networks: + - rabbitmq + +networks: + rabbitmq: + external: true diff --git a/docker/dct/constants.ts b/docker/dct/constants.ts new file mode 100644 index 00000000..2f13b0c3 --- /dev/null +++ b/docker/dct/constants.ts @@ -0,0 +1,29 @@ +export const sharedPHPDependencies = [ + `just`, + `mariadb-client`, + `php83-bcmath`, + `php83-common`, + `php83-curl`, + `php83-dom`, + `php83-iconv`, + `php83-mbstring`, + `php83-mysqli`, + `php83-mysqlnd`, + `php83-openssl`, + `php83-phar`, + `php83-session`, + `php83-simplexml`, + `php83-sockets`, + `php83-zip`, + `php83`, + `python3`, + `wget` +] + +export const buildDependencies = [...sharedPHPDependencies, `alpine-sdk`, `bash`, `git`, `php83-tokenizer`, `php83-xml`, `php83-xmlwriter`] + +export const backendDependencies = [...sharedPHPDependencies, `php83-fpm`] + +export const appPath = `/app` +export const buildPath = `/build` +export const nginxWebPath = `/var/www/html` diff --git a/docker/dct/generate-dockerfile.ts b/docker/dct/generate-dockerfile.ts new file mode 100755 index 00000000..78079f45 --- /dev/null +++ b/docker/dct/generate-dockerfile.ts @@ -0,0 +1,54 @@ +import { DockerConfigTool } from '@tyisi/docker-config-tool-js' +import { buildDependencies, appPath, buildPath, backendDependencies, nginxWebPath } from './constants' +import { createSourceStage } from './stages/source' +import { createBaseStage } from './stages/base' +import { createBuildBaseStage } from './stages/build-base' +import { createBuildStage } from './stages/build' +import { createBuildBackendPHPStageStage } from './stages/build-backed-php' +import { createBuildFrontendReactStageStage } from './stages/build-frontend-react' +import { createBuildFrontendWebStageStage } from './stages/build-frontend-web' +import { createBuildWebhookerStageStage } from './stages/build-webhooker' +import { createBackendPHPBaseStage } from './stages/backend-php-base' +import { createBackendPHPStage } from './stages/backend-php' +import { createFrontendReactStage } from './stages/frontend-react' +import { createFrontendWebStage } from './stages/frontend-web' +import { createWebHookerBaseStage } from './stages/webhooker-base' +import { createWebHookerStage } from './stages/webhooker' + +// Constants + +// Generate Dockerfile + +const dct = new DockerConfigTool() + +dct.withArg(`PHP_VERSION=8.3`) + +const sourceStage = createSourceStage(dct) + +const baseStage = createBaseStage(dct) + +const buildBaseStage = createBuildBaseStage(dct, { baseStage }) + +const buildStage = createBuildStage(dct, { buildBaseStage, sourceStage }) + +const buildBackendPHPStage = createBuildBackendPHPStageStage(dct, { buildStage }) + +const buildFrontendReactStage = createBuildFrontendReactStageStage(dct, { buildStage }) + +const buildFrontendWebStage = createBuildFrontendWebStageStage(dct, { buildStage }) + +const buildWebhookerStage = createBuildWebhookerStageStage(dct, { buildStage }) + +const backendPHPBaseStage = createBackendPHPBaseStage(dct) + +const webHookerBaseStage = createWebHookerBaseStage(dct, { baseStage }) + +const backendPHPStage = createBackendPHPStage(dct, { backendPHPBaseStage, buildBackendPHPStage, sourceStage }) + +const frontendReactStage = createFrontendReactStage(dct, { buildFrontendReactStage }) + +const frontendWebStage = createFrontendWebStage(dct, { buildFrontendWebStage }) + +const webHookerStage = createWebHookerStage(dct, { webHookerBaseStage, buildWebhookerStage }) + +console.log(dct.toString()) diff --git a/docker/dct/stages/backend-php-base.ts b/docker/dct/stages/backend-php-base.ts new file mode 100644 index 00000000..ef4c15e5 --- /dev/null +++ b/docker/dct/stages/backend-php-base.ts @@ -0,0 +1,11 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { backendDependencies } from '../constants' + +export const createBackendPHPBaseStage = (dct: DockerConfigTool): IStage => { + const backendPHPBaseStage = dct.withStage({ from: `alpine:3`, as: `backend-php-base` }) + + backendPHPBaseStage.withRun(`apk --no-cache add`, ...backendDependencies) + backendPHPBaseStage.withRun(`sed -i 's/^listen/;listen/g' /etc/php83/php-fpm.d/www.conf`) + + return backendPHPBaseStage +} diff --git a/docker/dct/stages/backend-php.ts b/docker/dct/stages/backend-php.ts new file mode 100644 index 00000000..6bfa69d1 --- /dev/null +++ b/docker/dct/stages/backend-php.ts @@ -0,0 +1,32 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { nginxWebPath, buildPath, appPath } from '../constants' + +export const createBackendPHPStage = ( + dct: DockerConfigTool, + { backendPHPBaseStage, buildBackendPHPStage, sourceStage }: Record +): IStage => { + const backendPHPStage = dct.withStage({ from: backendPHPBaseStage, as: `backend-php` }) + + backendPHPStage.withVolume(`/sessions`) + backendPHPStage.withExpose(9000) + backendPHPStage.withWorkDir(`${nginxWebPath}`) + backendPHPStage.withVolume(`${nginxWebPath}/backup`) + backendPHPStage.withRun(`mkdir -p ${nginxWebPath}/backup && chown nobody:nobody ${nginxWebPath}/backup`) + backendPHPStage.withEntryPoint(`docker_compose_run.sh`) + backendPHPStage.withCmd(`php-fpm83`) + backendPHPStage.withRun(`mkdir -p ${nginxWebPath}/conf/`) + + backendPHPStage.withCopy(`${buildPath}/migrations/`, `migrations/`).setFrom(sourceStage).setLinked() + + backendPHPStage.withCopy(`${appPath}/backend-php${appPath}/`, `app/`).setFrom(buildBackendPHPStage).setLinked() + backendPHPStage.withCopy(`${appPath}/backend-php/conf/config.docker.ini.php`, `conf/config.ini.php`).setFrom(buildBackendPHPStage).setLinked() + backendPHPStage.withCopy(`${appPath}/backend-php/tools/`, `tools/`).setFrom(buildBackendPHPStage).setLinked() + backendPHPStage.withCopy(`${appPath}/backend-php/vhs/`, `vhs/`).setFrom(buildBackendPHPStage).setLinked() + backendPHPStage.withCopy(`${appPath}/backend-php/vendor/`, `vendor/`).setFrom(buildBackendPHPStage).setLinked() + + backendPHPStage.withCopy(`${appPath}/backend-php/conf/php/*.ini`, `/etc/php83/conf.d/`).setFrom(buildBackendPHPStage).setLinked() + backendPHPStage.withCopy(`${appPath}/backend-php/conf/php-fpm/*.conf`, `/etc/php83/php-fpm.d/`).setFrom(buildBackendPHPStage).setLinked() + backendPHPStage.withCopy(`${appPath}/backend-php/docker/*.sh`, `/usr/local/bin/`).setFrom(buildBackendPHPStage).setLinked() + + return backendPHPStage +} diff --git a/docker/dct/stages/base.ts b/docker/dct/stages/base.ts new file mode 100644 index 00000000..023a23ea --- /dev/null +++ b/docker/dct/stages/base.ts @@ -0,0 +1,12 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' + +export const createBaseStage = (dct: DockerConfigTool): IStage => { + const baseStage = dct.withStage({ from: `node:22-alpine`, as: `base` }) + + baseStage.withEnv(`PNPM_HOME="/pnpm"`) + baseStage.withEnv(`PATH="$PNPM_HOME:$PATH"`) + + baseStage.withRun(`npm install -g npm@latest && npm install -g corepack@latest`) + + return baseStage +} diff --git a/docker/dct/stages/build-backed-php.ts b/docker/dct/stages/build-backed-php.ts new file mode 100644 index 00000000..99c10865 --- /dev/null +++ b/docker/dct/stages/build-backed-php.ts @@ -0,0 +1,11 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { buildPath, appPath } from '../constants' + +export const createBuildBackendPHPStageStage = (dct: DockerConfigTool, { buildStage }: Record): IStage => { + const buildBackendPHPStage = dct.withStage({ from: `scratch`, as: `build-backend-php` }) + + buildBackendPHPStage.withWorkDir(buildPath) + buildBackendPHPStage.withCopy(`/build/packages/backend-php/`, `${appPath}/backend-php/`).setLinked().setFrom(buildStage) + + return buildBackendPHPStage +} diff --git a/docker/dct/stages/build-base.ts b/docker/dct/stages/build-base.ts new file mode 100644 index 00000000..8b7d88af --- /dev/null +++ b/docker/dct/stages/build-base.ts @@ -0,0 +1,10 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { buildDependencies } from '../constants' + +export const createBuildBaseStage = (dct: DockerConfigTool, { baseStage }: Record): IStage => { + const buildBaseStage = dct.withStage({ from: baseStage, as: `build-base` }) + + buildBaseStage.withRun([`apk --no-cache add`, ...buildDependencies]) + + return buildBaseStage +} diff --git a/docker/dct/stages/build-frontend-react.ts b/docker/dct/stages/build-frontend-react.ts new file mode 100644 index 00000000..79a00e5d --- /dev/null +++ b/docker/dct/stages/build-frontend-react.ts @@ -0,0 +1,12 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { buildPath, appPath } from '../constants' + +export const createBuildFrontendReactStageStage = (dct: DockerConfigTool, { buildStage }: Record): IStage => { + const buildFrontendReactStage = dct.withStage({ from: `scratch`, as: `build-frontend-react` }) + + buildFrontendReactStage.withWorkDir(buildPath) + buildFrontendReactStage.withCopy(`/build/packages/frontend-react/conf/`, `${appPath}/frontend-react/conf/`).setLinked().setFrom(buildStage) + buildFrontendReactStage.withCopy(`/build/packages/frontend-react/dist/`, `${appPath}/frontend-react/dist/`).setLinked().setFrom(buildStage) + + return buildFrontendReactStage +} diff --git a/docker/dct/stages/build-frontend-web.ts b/docker/dct/stages/build-frontend-web.ts new file mode 100644 index 00000000..a55c2733 --- /dev/null +++ b/docker/dct/stages/build-frontend-web.ts @@ -0,0 +1,11 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { buildPath, appPath } from '../constants' + +export const createBuildFrontendWebStageStage = (dct: DockerConfigTool, { buildStage }: Record): IStage => { + const buildFrontendWebStage = dct.withStage({ from: `scratch`, as: `build-frontend-web` }) + + buildFrontendWebStage.withWorkDir(buildPath) + buildFrontendWebStage.withCopy(`/build/packages/frontend-web/`, `${appPath}/frontend-web/`).setLinked().setFrom(buildStage) + + return buildFrontendWebStage +} diff --git a/docker/dct/stages/build-webhooker.ts b/docker/dct/stages/build-webhooker.ts new file mode 100644 index 00000000..cfa7b23b --- /dev/null +++ b/docker/dct/stages/build-webhooker.ts @@ -0,0 +1,20 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { buildPath, appPath } from '../constants' + +export const createBuildWebhookerStageStage = (dct: DockerConfigTool, { buildStage }: Record): IStage => { + const buildWebhookerStage = dct.withStage({ from: buildStage, as: `build-webhooker` }) + + buildWebhookerStage.withWorkDir(buildPath) + buildWebhookerStage.withRun({ + commands: [`pnpm deploy --filter="@vhs/webhooker" --prod ${appPath}/webhooker/`], + mount: { + type: `cache`, + id: `nomos-pnpm`, + target: `/pnpm/store`, + uid: 1000, + gid: 1000 + } + }) + + return buildWebhookerStage +} diff --git a/docker/dct/stages/build.ts b/docker/dct/stages/build.ts new file mode 100644 index 00000000..47110718 --- /dev/null +++ b/docker/dct/stages/build.ts @@ -0,0 +1,25 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { appPath, buildPath } from '../constants' + +export const createBuildStage = (dct: DockerConfigTool, { buildBaseStage, sourceStage }: Record): IStage => { + const buildStage = dct.withStage({ from: buildBaseStage, as: `build` }) + + buildStage.withEnv(`COMPOSER_INSTALL_OPT=--no-dev`) + buildStage.withEnv(`CI=true`) + buildStage.withRun(`mkdir ${appPath}/ /build/ && chown 1000:1000 /build/ ${appPath}/`) + buildStage.withUser({ uid: 1000, gid: 1000 }) + buildStage.withCopy({ sources: buildPath, destination: buildPath }).setFrom(sourceStage).setLinked().setChown(`1000:1000`) + buildStage.withWorkDir(buildPath) + buildStage.withRun({ + commands: [`pnpm install --frozen-lockfile && pnpm -r build`], + mount: { + type: `cache`, + id: `nomos-pnpm`, + target: `/pnpm/store`, + uid: 1000, + gid: 1000 + } + }) + + return buildStage +} diff --git a/docker/dct/stages/frontend-react.ts b/docker/dct/stages/frontend-react.ts new file mode 100644 index 00000000..5e39905c --- /dev/null +++ b/docker/dct/stages/frontend-react.ts @@ -0,0 +1,14 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { appPath, nginxWebPath } from '../constants' + +export const createFrontendReactStage = (dct: DockerConfigTool, { buildFrontendReactStage }: Record): IStage => { + const frontendReactStage = dct.withStage({ from: `nginx:stable-alpine`, as: `frontend-react` }) + + frontendReactStage + .withCopy(`${appPath}/frontend-react/conf/nginx-react-docker-compose.conf`, `/etc/nginx/conf.d/default.conf`) + .setLinked() + .setFrom(buildFrontendReactStage) + frontendReactStage.withCopy(`${appPath}/frontend-react/dist/`, `${nginxWebPath}/`).setLinked().setFrom(buildFrontendReactStage) + + return frontendReactStage +} diff --git a/docker/dct/stages/frontend-web.ts b/docker/dct/stages/frontend-web.ts new file mode 100644 index 00000000..bd9532a4 --- /dev/null +++ b/docker/dct/stages/frontend-web.ts @@ -0,0 +1,14 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { appPath, nginxWebPath } from '../constants' + +export const createFrontendWebStage = (dct: DockerConfigTool, { buildFrontendWebStage }: Record): IStage => { + const frontendWebStage = dct.withStage({ from: `nginx:stable-alpine`, as: `frontend-web` }) + + frontendWebStage + .withCopy(`${appPath}/frontend-web/conf/nginx-vhost-docker-compose.conf`, `/etc/nginx/conf.d/default.conf`) + .setLinked() + .setFrom(buildFrontendWebStage) + frontendWebStage.withCopy(`${appPath}/frontend-web/web/`, `${nginxWebPath}/`).setLinked().setFrom(buildFrontendWebStage) + + return frontendWebStage +} diff --git a/docker/dct/stages/source.ts b/docker/dct/stages/source.ts new file mode 100644 index 00000000..a868292b --- /dev/null +++ b/docker/dct/stages/source.ts @@ -0,0 +1,10 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { buildPath } from '../constants' + +export const createSourceStage = (dct: DockerConfigTool): IStage => { + const sourceStage = dct.withStage({ from: `scratch`, as: `source` }) + sourceStage.withWorkDir(buildPath) + sourceStage.withCopy(`./`, `./`).setLinked() + + return sourceStage +} diff --git a/docker/dct/stages/webhooker-base.ts b/docker/dct/stages/webhooker-base.ts new file mode 100644 index 00000000..3ff5d5e0 --- /dev/null +++ b/docker/dct/stages/webhooker-base.ts @@ -0,0 +1,9 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' + +export const createWebHookerBaseStage = (dct: DockerConfigTool, { baseStage }: Record): IStage => { + const webHookerBaseStage = dct.withStage({ from: baseStage, as: `webhooker-base` }) + + webHookerBaseStage.withRun(`apk --no-cache add bash`) + + return webHookerBaseStage +} diff --git a/docker/dct/stages/webhooker.ts b/docker/dct/stages/webhooker.ts new file mode 100644 index 00000000..8296293d --- /dev/null +++ b/docker/dct/stages/webhooker.ts @@ -0,0 +1,15 @@ +import { DockerConfigTool, IStage } from '@tyisi/docker-config-tool-js' +import { appPath } from '../constants' + +export const createWebHookerStage = (dct: DockerConfigTool, { webHookerBaseStage, buildWebhookerStage }: Record): IStage => { + const webHookerStage = dct.withStage({ from: webHookerBaseStage, as: `webhooker` }) + + webHookerStage.withUser({ uid: 1000, gid: 1000 }) + webHookerStage.withWorkDir(appPath) + webHookerStage.withRun(`mkdir -p ${appPath}/logs ${appPath}/webhooker && chown 1000:1000 ${appPath}/logs ${appPath}/webhooker`) + webHookerStage.withWorkDir(`${appPath}/webhooker`) + webHookerStage.withCopy(`${appPath}/webhooker/`, `${appPath}/webhooker/`).setLinked().setFrom(buildWebhookerStage) + webHookerStage.withCmd(`${appPath}/webhooker/webhooker.sbin`) + + return webHookerStage +} diff --git a/docker/docker_compose_run.sh b/docker/docker_compose_run.sh deleted file mode 100755 index 652b7111..00000000 --- a/docker/docker_compose_run.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -docker_env_config.sh > /var/www/html/conf/env.php - -(cd /var/www/html/tools && php migrate.php -b -m -t) - -exec /usr/local/bin/docker-php-entrypoint "$@" diff --git a/docker/docker_env_config.sh b/docker/docker_env_config.sh deleted file mode 100755 index 91543718..00000000 --- a/docker/docker_env_config.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo " /www/conf/env.php - -service php5-fpm start -nginx diff --git a/docker/nomos.env.template b/docker/nomos.env.template index b71993ed..491aa465 100644 --- a/docker/nomos.env.template +++ b/docker/nomos.env.template @@ -1,4 +1,4 @@ -NOMOS_DB_SERVER=nomos-mysql +NOMOS_DB_SERVER=mysqld NOMOS_DB_USER=nomos NOMOS_DB_PASSWORD=Password1 NOMOS_DB_DATABASE=nomos @@ -18,7 +18,7 @@ NOMOS_STRIPE_API_KEY= NOMOS_STRIPE_WEBHOOK_SECRET= NOMOS_STRIPE_PRODUCTS= -NOMOS_RABBITMQ_HOST=nomos-rabbitmq +NOMOS_RABBITMQ_HOST=rabbitmq NOMOS_RABBITMQ_PORT=5672 NOMOS_RABBITMQ_USER=nomos NOMOS_RABBITMQ_PASSWORD=Password1 diff --git a/generate-diff-report.sh b/generate-diff-report.sh new file mode 100755 index 00000000..c27aa3b8 --- /dev/null +++ b/generate-diff-report.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +git diff --numstat origin/master..HEAD | perl -pe 's/\s{1,}=>\s{1,}/=>/g' | column -t | grep -vw v2 | grep -vw packages | grep -v ' 0 ' | grep -v '^0' | awk '{ print $3 }' | grep -v -E '^\+ +(\/\*\*|\*)' | grep -v '=>' | grep -P '^(app|tests|vhs)\/' | while read -r CHANGED_FILE; do + + RESULT=$(git diff origin/master..HEAD "${CHANGED_FILE}" | grep -P '^[-+]' | grep -v -P '^[-+]{3}' | grep -v -P '^[-+]($|\/\/|\/\*\*|\s+(\/\*\*|\*|\/\/)| public static function Define| .+con?vertType)') + + if [ "${RESULT}" != "" ]; then + echo "============================================================" + echo "========== ${CHANGED_FILE}" + echo "============================================================" + echo "${RESULT}" + fi +done > diff-report.txt && echo "$(grep -c -P '^========== (app|tests|vhs)\/' diff-report.txt) files changed" diff --git a/justfile b/justfile index 8c2cc088..fa98567f 100644 --- a/justfile +++ b/justfile @@ -1,78 +1,89 @@ -set export - +set export := true help: - just -l + @just -l + +build target: + @echo 'Building {{target}}…' + @just "build_{{target}}" + +build_all: + pnpm --filter "./packages/*/" build format target: @echo 'Formatting {{target}}…' - just "format_{{target}}" + @just "format_{{target}}" format_all: #!/usr/bin/env bash - set -eo pipefail - node_modules/.bin/prettier -w ${FILES:-.} + echo ${FILES:-.} | xargs -n1 | grep -v -E $(find . -type l | grep -vw node_modules | cut -f2- -d/ | xargs | tr ' ' '|') | xargs pnpm exec prettier -w format_php: - #!/usr/bin/env bash - set -eo pipefail + @pnpm --filter "./packages/*-php/" format:php - vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php ${FILES:-app/ tests/ tools/ vhs/} +git_hooks: + #!/usr/bin/env bash + echo "Available git hooks:" + @just --summary | xargs -d' ' -I% echo '- %' | grep 'git_hook_' -install_composer: +git_hook_pre_commit: #!/usr/bin/env bash - set -euo pipefail - if [ ! -f ./tools/composer.phar ]; then - TMPFILE=$(mktemp) + set -e + + FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') - EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')" - php -r "copy('https://getcomposer.org/installer', '${TMPFILE}');" - ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', '${TMPFILE}');")" + if [ "${FILES}" != "" ]; then + PHP_FILES=$(echo "${FILES}" | grep '\.php' | xargs) + STORYBOOK_FILES=$(echo "${FILES}" | grep -E 'packages/frontend-react/.+\.stories\.tsx' | xargs) + VALIDATOR_FILES=$(echo "${FILES}" | grep -E 'packages/frontend-react/src/lib/validators/(common|records).ts' | xargs) + WEBHOOKER_FILES=$(echo "${FILES}" | grep 'packages/webhooker/' | xargs) - if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then - echo >&2 'ERROR: Invalid installer checksum' - rm "${TMPFILE}" - exit 1 + if [ "${PHP_FILES}" != "" ]; then + FILES=$(echo "${PHP_FILES}" | xargs) pnpm exec just format php + pnpm exec just test php fi - php "${TMPFILE}" --install-dir ./tools --quiet - rm "${TMPFILE}" - else - echo "composer has already been set up!" - fi + if [ "${STORYBOOK_FILES}" != "" ]; then + pnpm --filter @vhs/nomos-frontend-react fix:storybook:titles + fi + + if [ "${VALIDATOR_FILES}" != "" ]; then + pnpm --filter @vhs/nomos-frontend-react generate:validator:implementations + fi + + if [ "${WEBHOOKER_FILES}" != "" ]; then + pnpm --filter="@vhs/webhooker" test + fi + + FILES=$(echo "${FILES}" | xargs) pnpm exec just format all - ./tools/composer.sh install + git update-index --again + + exit 0 + fi -install_angular_ui_bootstrap: +git_hook_pre_push: #!/usr/bin/env bash - set -euo pipefail - mkdir -p web/components/custom/angular-ui/ \ - && cd web/components/custom/angular-ui/ \ - && wget https://raw.githubusercontent.com/angular-ui/bootstrap/refs/heads/gh-pages/ui-bootstrap-tpls-0.12.0.js \ - && wget https://raw.githubusercontent.com/angular-ui/bootstrap/refs/heads/gh-pages/ui-bootstrap-tpls-0.12.0.min.js + set -e -install_webcomponents: install_angular_ui_bootstrap + if git show-ref "$(git branch --show-current)" | awk '{ print $2 }' | xargs | sed 's/ /../g' | xargs git diff --numstat | grep packages/frontend-react > /dev/null; then + pnpm --filter @vhs/nomos-frontend-react build + fi -make_webcomponents_directories: - mkdir -p web/components/bower - mkdir -p web/components/custom + exit 0 -run_bower: - echo "Running bower" - ./tools/bower.sh install +install target: + @echo 'Installing {{target}}…' + @just "install_{{target}}" -run_composer: - echo "Running composer" - ./tools/composer.sh install +prepare: setup_husky setup target: @echo 'Setting up {{target}}…' - just "setup_{{target}}" - -setup_webcomponents: make_webcomponents_directories install_webcomponents run_bower + @just "setup_{{target}}" setup_husky: #!/usr/bin/env bash @@ -80,32 +91,20 @@ setup_husky: if [ ! -d .husky/_/ ]; then node_modules/.bin/husky + elif [ "$(grep 'hooksPath = .husky/_' .git/config)" = "" ] ; then + node_modules/.bin/husky else echo "husky has already been set up!" fi -setup_vendor: install_composer run_composer - -setup_webhooker: - #!/usr/bin/env bash - set -euo pipefail - - cd webhooker/ && npm install - test target: @echo 'Testing {{target}}…' - just "test_{{target}}" - -test_php: - #!/usr/bin/env bash - set -eo pipefail + @just "test_{{target}}" - vendor/bin/phpunit ${FILES:-app/ tests/ tools/ vhs/} +test_all: + @echo "Testing all packages..." + @pnpm --filter "./packages/*/" test:php -test_webhooker: - #!/usr/bin/env bash - set -eo pipefail - - if [ "${FILES}" != "" ] ; then - cd webhooker && npm run test - fi +test_php: + @echo "Testing all php packages..." + @pnpm --filter "./packages/*-php/" test:php diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6389fe42..00000000 --- a/package-lock.json +++ /dev/null @@ -1,5277 +0,0 @@ -{ - "name": "@vhs/nomos", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@vhs/nomos", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@types/lodash": "^4.17.13", - "just-install": "^2.0.2", - "wireit": "^0.14.9" - }, - "devDependencies": { - "@prettier/plugin-php": "^0.22.2", - "@prettier/plugin-xml": "^3.4.1", - "@tyisi/config-eslint": "^4.0.0", - "@tyisi/config-prettier": "^1.0.1", - "@types/angular": "^1.8.9", - "@types/bootstrap": "^5.2.10", - "@types/jquery": "^3.5.32", - "bower": "^1.8.14", - "eslint": "^9.15.0", - "husky": "^9.1.7", - "prettier": "^3.0.3", - "prettier-plugin-sh": "^0.14.0", - "prettier-plugin-sql": "^0.18.1", - "typescript": "^5.7.2" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@prettier/plugin-php": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.22.2.tgz", - "integrity": "sha512-md0+7tNbsP0oy+wIP3KZZc6fzx1k1jtWaMjOy/gM8yU9f2BDYEi+iHOc/UNPihYvPI28zFTbjvlhH4QXQjQwNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "linguist-languages": "^7.27.0", - "php-parser": "^3.1.5" - }, - "peerDependencies": { - "prettier": "^3.0.0" - } - }, - "node_modules/@prettier/plugin-xml": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@prettier/plugin-xml/-/plugin-xml-3.4.1.tgz", - "integrity": "sha512-Uf/6/+9ez6z/IvZErgobZ2G9n1ybxF5BhCd7eMcKqfoWuOzzNUxBipNo3QAP8kRC1VD18TIo84no7LhqtyDcTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xml-tools/parser": "^1.0.11" - }, - "peerDependencies": { - "prettier": "^3.0.0" - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@tyisi/config-eslint": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@tyisi/config-eslint/-/config-eslint-4.0.0.tgz", - "integrity": "sha512-d3mQl5w+D8EQ5gKjEOKMozAnfqqlcQkHEDP/ihrQQvR7t2rIsJu9xIxhJxRv8viWrk4kVVamYpOe410o6rCblQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@eslint/js": "^9.12.0", - "deepmerge": "^4.3.1", - "eslint-config-love": "^87.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.1", - "globals": "^15.11.0", - "rust-just": "^1.36.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "eslint": "^9.12.0", - "prettier": "^3.3.3" - } - }, - "node_modules/@tyisi/config-prettier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tyisi/config-prettier/-/config-prettier-1.0.1.tgz", - "integrity": "sha512-tRmvt+2S4XpB7ibEMOXM+Unru6jrxk/V2FDXYO/2wgYnj5EVxkinT1U0XU2Gj4AuuHpbCdvksX1vbJvfMdmR+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "prettier-plugin-sh": "^0.14.0", - "prettier-plugin-tailwindcss": "^0.6.8" - }, - "peerDependencies": { - "prettier": "^3.3.3" - } - }, - "node_modules/@types/angular": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.9.tgz", - "integrity": "sha512-Z0HukqZkx0fotsV3QO00yqU9NzcQI+tMcrum+8MvfB4ePqCawZctF/gz6QiuII+T1ax+LitNoPx/eICTgnF4sg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/bootstrap": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", - "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@popperjs/core": "^2.9.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jquery": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", - "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, - "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/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", - "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/sizzle": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", - "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", - "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/type-utils": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", - "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", - "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", - "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", - "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", - "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", - "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@xml-tools/parser": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", - "integrity": "sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "chevrotain": "7.1.1" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "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/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", - "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bower": { - "version": "1.8.14", - "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.14.tgz", - "integrity": "sha512-8Rq058FD91q9Nwthyhw0la9fzpBz0iwZTrt51LWl+w+PnJgZk9J+5wp3nibsJcIUPglMYXr4NRBaR+TUj0OkBQ==", - "dev": true, - "license": "MIT", - "bin": { - "bower": "bin/bower" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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/chevrotain": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", - "integrity": "sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "regexp-to-ast": "0.5.0" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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/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 - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "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/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", - "dev": true - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.23.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.5.tgz", - "integrity": "sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.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-iterator-helpers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", - "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.3", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", - "@eslint/plugin-kit": "^0.2.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-love": { - "version": "87.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-love/-/eslint-config-love-87.0.0.tgz", - "integrity": "sha512-WqVVU+z+OGzoo1Zl92+ZbD0ShQzYvdLDH8L5F6c432MOAWlDp7mtqg5HuAdjPAMWiKtfzB0+0hVLzcrikd5whQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mightyiam" - }, - "https://wise.com/pay/me/shaharo" - ], - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^8.3.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-n": "^17.0.0", - "eslint-plugin-promise": "^7.0.0", - "typescript-eslint": "^8.3.0" - }, - "peerDependencies": { - "eslint": "^9.0.0", - "typescript": "*" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", - "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.3.5", - "enhanced-resolve": "^5.15.0", - "eslint-module-utils": "^2.8.1", - "fast-glob": "^3.3.2", - "get-tsconfig": "^4.7.5", - "is-bun-module": "^1.0.2", - "is-glob": "^4.0.3" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", - "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/ota-meshi", - "https://opencollective.com/eslint" - ], - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.11.0", - "eslint-compat-utils": "^0.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/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/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-n": { - "version": "17.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.14.0.tgz", - "integrity": "sha512-maxPLMEA0rPmRpoOlxEclKng4UpDe+N5BJS4t24I3UKnN109Qcivnfs37KMy84G0af3bxjog5lKctP5ObsvcTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.1", - "enhanced-resolve": "^5.17.1", - "eslint-plugin-es-x": "^7.8.0", - "get-tsconfig": "^4.8.1", - "globals": "^15.11.0", - "ignore": "^5.3.2", - "minimatch": "^9.0.5", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": ">=8.23.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz", - "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", - "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.1.0", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.8", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/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/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "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/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/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/execa": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", - "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.3", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^8.0.0", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^6.0.0", - "pretty-ms": "^9.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.5.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "15.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", - "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/human-signals": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", - "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bun-module": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", - "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz", - "integrity": "sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "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/iterator.prototype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", - "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "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/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" - }, - "node_modules/jsox": { - "version": "1.2.119", - "resolved": "https://registry.npmjs.org/jsox/-/jsox-1.2.119.tgz", - "integrity": "sha512-f37obwxWKKuylcaOzNlUlzfDvURSCpqTXs8yEivhvsp86D/DTIySxP4v5Qdlg24qCuzDSZ0mJr3krc/f7TZ/5A==", - "dev": true, - "bin": { - "jsox": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/just-install": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/just-install/-/just-install-2.0.2.tgz", - "integrity": "sha512-zH6aon3V2P8ZbD+njaMB/orHsOyFMgONSpxKtbovNu7Bhb1rD9qhnMkT2Nj91++b9GgqHNbozhUdIMxecmWJaw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "extract-zip": "^2.0.1", - "node-fetch": "^3.3.2" - }, - "bin": { - "just": "bin/just.js", - "just-install": "install.js" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/linguist-languages": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-7.27.0.tgz", - "integrity": "sha512-Wzx/22c5Jsv2ag+uKy+ITanGA5hzvBZngrNGDXLTC7ZjGM6FLCYGgomauTkxNJeP9of353OM0pWqngYA180xgw==", - "dev": true, - "license": "MIT" - }, - "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.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/moo": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", - "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", - "dev": true - }, - "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==", - "license": "MIT" - }, - "node_modules/mvdan-sh": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz", - "integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nearley": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", - "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", - "dev": true, - "dependencies": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6" - }, - "bin": { - "nearley-railroad": "bin/nearley-railroad.js", - "nearley-test": "bin/nearley-test.js", - "nearley-unparse": "bin/nearley-unparse.js", - "nearleyc": "bin/nearleyc.js" - }, - "funding": { - "type": "individual", - "url": "https://nearley.js.org/#give-to-nearley" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-sql-parser": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.18.0.tgz", - "integrity": "sha512-2YEOR5qlI1zUFbGMLKNfsrR5JUvFg9LxIRVE+xJe962pfVLH0rnItqLzv96XVs1Y1UIR8FxsXAuvX/lYAWZ2BQ==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.48" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/php-parser": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.5.tgz", - "integrity": "sha512-jEY2DcbgCm5aclzBdfW86GM6VEIWcSlhTBSHN1qhJguVePlYe28GhwS0yoeLYXpM2K8y6wzLwrbq814n2PHSoQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-sh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.14.0.tgz", - "integrity": "sha512-hfXulj5+zEl/ulrO5kMuuTPKmXvOg0bnLHY1hKFNN/N+/903iZbNp8NyZBTsgI8dtkSgFfAEIQq0IQTyP1ZVFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mvdan-sh": "^0.10.1", - "sh-syntax": "^0.4.1" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - }, - "peerDependencies": { - "prettier": "^3.0.3" - } - }, - "node_modules/prettier-plugin-sql": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-sql/-/prettier-plugin-sql-0.18.1.tgz", - "integrity": "sha512-2+Nob2sg7hzLAKJoE6sfgtkhBZCqOzrWHZPvE4Kee/e80oOyI4qwy9vypeltqNBJwTtq3uiKPrCxlT03bBpOaw==", - "dev": true, - "dependencies": { - "jsox": "^1.2.119", - "node-sql-parser": "^4.12.0", - "sql-formatter": "^15.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - }, - "peerDependencies": { - "prettier": "^3.0.3" - } - }, - "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz", - "integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "@ianvs/prettier-plugin-sort-imports": "*", - "@prettier/plugin-pug": "*", - "@shopify/prettier-plugin-liquid": "*", - "@trivago/prettier-plugin-sort-imports": "*", - "@zackad/prettier-plugin-twig-melody": "*", - "prettier": "^3.0", - "prettier-plugin-astro": "*", - "prettier-plugin-css-order": "*", - "prettier-plugin-import-sort": "*", - "prettier-plugin-jsdoc": "*", - "prettier-plugin-marko": "*", - "prettier-plugin-multiline-arrays": "*", - "prettier-plugin-organize-attributes": "*", - "prettier-plugin-organize-imports": "*", - "prettier-plugin-sort-imports": "*", - "prettier-plugin-style-order": "*", - "prettier-plugin-svelte": "*" - }, - "peerDependenciesMeta": { - "@ianvs/prettier-plugin-sort-imports": { - "optional": true - }, - "@prettier/plugin-pug": { - "optional": true - }, - "@shopify/prettier-plugin-liquid": { - "optional": true - }, - "@trivago/prettier-plugin-sort-imports": { - "optional": true - }, - "@zackad/prettier-plugin-twig-melody": { - "optional": true - }, - "prettier-plugin-astro": { - "optional": true - }, - "prettier-plugin-css-order": { - "optional": true - }, - "prettier-plugin-import-sort": { - "optional": true - }, - "prettier-plugin-jsdoc": { - "optional": true - }, - "prettier-plugin-marko": { - "optional": true - }, - "prettier-plugin-multiline-arrays": { - "optional": true - }, - "prettier-plugin-organize-attributes": { - "optional": true - }, - "prettier-plugin-organize-imports": { - "optional": true - }, - "prettier-plugin-sort-imports": { - "optional": true - }, - "prettier-plugin-style-order": { - "optional": true - }, - "prettier-plugin-svelte": { - "optional": true - } - } - }, - "node_modules/pretty-ms": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", - "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", - "dev": true - }, - "node_modules/randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "dependencies": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz", - "integrity": "sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "which-builtin-type": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp-to-ast": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", - "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "dev": true, - "license": "MIT" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rust-just": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just/-/rust-just-1.37.0.tgz", - "integrity": "sha512-KQ9kRqxgncSM3ALWJxhEx86DiHBu7/mn07KP/A7itJ5ALmO+SfjuBi2icOzpz7tsZKT1A5UZ8Z0Iw8iB+Om9vA==", - "dev": true, - "license": "CC0-1.0", - "dependencies": { - "execa": "^9.4.1", - "yargs": "^17.7.2" - }, - "bin": { - "rust-just": "lib/index.mjs" - }, - "engines": { - "node": ">=18.19 || >=20.6 || >=21" - }, - "optionalDependencies": { - "rust-just-darwin-arm64": "1.37.0", - "rust-just-darwin-x64": "1.37.0", - "rust-just-linux-arm": "1.37.0", - "rust-just-linux-arm64": "1.37.0", - "rust-just-linux-x64": "1.37.0", - "rust-just-windows-arm64": "1.37.0", - "rust-just-windows-x64": "1.37.0" - } - }, - "node_modules/rust-just-darwin-arm64": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just-darwin-arm64/-/rust-just-darwin-arm64-1.37.0.tgz", - "integrity": "sha512-Q/0cU7olKzoySSgQfVyMRRaiUYr2q3qoCslUhpBIIE6Tn/rDBz/rfTNIBqrFaRjPzPgRZb4LFeBz2cdTMLQ4zw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "CC0-1.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/rust-just-darwin-x64": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just-darwin-x64/-/rust-just-darwin-x64-1.37.0.tgz", - "integrity": "sha512-pCmWdyzwxbPZEHJ38ymgEUQ28vzEE0YPY9N7+hAGnpycsBII6aV58+knCXllfxHDRgWo26MB0XPLtKtrpsIQFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "CC0-1.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/rust-just-linux-arm": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just-linux-arm/-/rust-just-linux-arm-1.37.0.tgz", - "integrity": "sha512-Y2lYZ5ysW9cytS8MsRj9VU1oTH30UvlwwaQcePgcZ3gAOTyK4O1/jzwTD/3BmYwmonPQilzgoi/Qp6XMkHMZhQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "CC0-1.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/rust-just-linux-arm64": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just-linux-arm64/-/rust-just-linux-arm64-1.37.0.tgz", - "integrity": "sha512-JsnC/ZiVospo2/rUr1HdsSRDFBbaIzAdT8+90sAxtXfhX/Whm34ePGMUyQ77EV22bNEquioqkALI2wgo+Wfkgw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "CC0-1.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/rust-just-linux-x64": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just-linux-x64/-/rust-just-linux-x64-1.37.0.tgz", - "integrity": "sha512-lDmG4RSSTkIQlWYLq+k4bpwSsMwIhEg1ffG6oa2vLSdZvAD6PEXhZ26x/kpRKc0r6vm1xqkOBzm988j5uWWtmA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "CC0-1.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/rust-just-windows-arm64": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just-windows-arm64/-/rust-just-windows-arm64-1.37.0.tgz", - "integrity": "sha512-Lr8zdhBSMsuivsdAkCoo4mdK1OuHf4zyqV1QONPFKuMeRRl1EI2Svi0M6GKTkioJY0FgdAdbvWpQBI9Zt9RtvA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "CC0-1.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/rust-just-windows-x64": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/rust-just-windows-x64/-/rust-just-windows-x64-1.37.0.tgz", - "integrity": "sha512-kT0Cr7c1UAL1MLI1L65caVwF3h9bKc3OKcveRIqC9917+Ft29ZH+hc8IsTqnKD6zmeVB8M9kAt3ijqReX5g/WA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "CC0-1.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sh-syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz", - "integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "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/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sql-formatter": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.2.tgz", - "integrity": "sha512-pNxSMf5DtwhpZ8gUcOGCGZIWtCcyAUx9oLgAtlO4ag7DvlfnETL0BGqXaISc84pNrXvTWmt8Wal1FWKxdTsL3Q==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1", - "get-stdin": "=8.0.0", - "nearley": "^2.20.1" - }, - "bin": { - "sql-formatter": "bin/sql-formatter-cli.cjs" - } - }, - "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/string-width/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/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", - "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/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/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", - "integrity": "sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz", - "integrity": "sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", - "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.16.0", - "@typescript-eslint/parser": "8.16.0", - "@typescript-eslint/utils": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "license": "MIT", - "optional": true - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "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/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.0.tgz", - "integrity": "sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wireit": { - "version": "0.14.9", - "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.14.9.tgz", - "integrity": "sha512-hFc96BgyslfO1WGSzQqOVYd5N3TB+4u9w70L9GHR/T7SYjvFmeznkYMsRIjMLhPcVabCEYPW1vV66wmIVDs+dQ==", - "license": "Apache-2.0", - "workspaces": [ - "vscode-extension", - "website" - ], - "dependencies": { - "brace-expansion": "^4.0.0", - "chokidar": "^3.5.3", - "fast-glob": "^3.2.11", - "jsonc-parser": "^3.0.0", - "proper-lockfile": "^4.1.2" - }, - "bin": { - "wireit": "bin/wireit.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/wireit/node_modules/balanced-match": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", - "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/wireit/node_modules/brace-expansion": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.0.tgz", - "integrity": "sha512-l/mOwLWs7BQIgOKrL46dIAbyCKvPV7YJPDspkuc88rHsZRlg3hptUGdU7Trv0VFP4d3xnSGBQrKu5ZvGB7UeIw==", - "license": "MIT", - "dependencies": { - "balanced-match": "^3.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "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/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "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" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", - "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index ffb34e45..219ba6ec 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "author": "", "dependencies": { - "@types/lodash": "^4.17.13", "just-install": "^2.0.2", - "wireit": "^0.14.9" + "wireit": "^0.14.12" }, "description": "NOMOS Our Membership Operations Software. In greek mythology, Nomos is the personified spirit of law.", "devDependencies": { @@ -11,15 +10,20 @@ "@prettier/plugin-xml": "^3.4.1", "@tyisi/config-eslint": "^4.0.0", "@tyisi/config-prettier": "^1.0.1", + "@tyisi/docker-config-tool-js": "^1.3.0", "@types/angular": "^1.8.9", "@types/bootstrap": "^5.2.10", "@types/jquery": "^3.5.32", + "@types/node": "^22.13.1", "bower": "^1.8.14", "eslint": "^9.15.0", "husky": "^9.1.7", "prettier": "^3.0.3", + "prettier-plugin-ini": "^1.3.0", + "prettier-plugin-nginx": "^1.0.3", "prettier-plugin-sh": "^0.14.0", "prettier-plugin-sql": "^0.18.1", + "prettier-plugin-tailwindcss": "^0.6.11", "typescript": "^5.7.2" }, "directories": { @@ -28,45 +32,25 @@ "keywords": [], "license": "ISC", "name": "@vhs/nomos", - "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf", + "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977", "private": true, "scripts": { - "prepare": "wireit", - "test": "wireit" + "prepare": "wireit" }, "version": "1.0.0", "wireit": { "prepare": { - "dependencies": [ - "prepare:webcomponents", - "prepare:husky", - "prepare:vendor", - "prepare:webhooker" - ] - }, - "prepare:husky": { - "command": "just setup husky" - }, - "prepare:vendor": { - "command": "just setup vendor" - }, - "prepare:webcomponents": { - "command": "just setup webcomponents" - }, - "prepare:webhooker": { - "command": "just setup webhooker" - }, - "test": { - "dependencies": [ - "test:php", - "test:webhooker" - ] - }, - "test:php": { - "command": "just test php" - }, - "test:webhooker": { - "command": "just test php" + "command": "just prepare" } + }, + "pnpm": { + "onlyBuiltDependencies": [ + "dtrace-provider", + "just-install", + "@parcel/watcher", + "@swc/core", + "esbuild", + "msw" + ] } } diff --git a/packages/backend-php/.gitignore b/packages/backend-php/.gitignore new file mode 100644 index 00000000..ae1834c7 --- /dev/null +++ b/packages/backend-php/.gitignore @@ -0,0 +1,68 @@ +* +!.gitignore + +!app/ +!app/**/ +!app/**/*.php + +!conf/ +!conf/**/ +!conf/**/*.conf +!conf/**/*.ini +!conf/**/*.php +!conf/mimes.types +conf/config.ini.php + +!docker/ +!docker/*.sh + +!tests/ +!tests/**/ +!tests/**/*.php + +!tools/ +!tools/**/ +!tools/backup/EMPTY +!tools/**/*.md +!tools/**/*.php +!tools/**/*.sh +tools/composer.phar + +!vhs/ +!vhs/**/ +!vhs/**/*.php + +!.bowerrc +!.circleci/config.yml +!.dockerignore +!.editorconfig +!.github/workflows/build-docker.yml +!.husky/pre-commit +!.npmrc +!.php-cs-fixer.php +!.prettierignore +!.shellcheckrc +!Dockerfile +!README.md +!Vagrantfile + +!bower.json +!circle.yml +!composer.json +!composer.lock + +!eslint.config.mjs +!justfile +nomos.d.ts +!package.json +!php-ts-transformer.php +!phpstan.neon +!phpunit.xml +!pnpm-lock.yaml +!pnpm-workspace.yaml +!psalm.xml +!prettier.config.mjs +!tsconfig.json + +!diff-report.txt +!generate-diff-report.sh diff --git a/.php-cs-fixer.php b/packages/backend-php/.php-cs-fixer.php similarity index 85% rename from .php-cs-fixer.php rename to packages/backend-php/.php-cs-fixer.php index be20281c..025a6873 100644 --- a/.php-cs-fixer.php +++ b/packages/backend-php/.php-cs-fixer.php @@ -25,7 +25,7 @@ ] ], // 'blank_line_after_opening_tag' => true, - // 'blank_line_before_statement' => true, + 'blank_line_before_statement' => true, 'braces' => [ 'allow_single_line_closure' => true ], @@ -46,7 +46,8 @@ 'new_with_braces' => false, 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, - // "no_blank_lines_before_namespace" => true, + 'blank_lines_before_namespace' => true, + 'no_blank_lines_before_namespace' => false, 'no_empty_comment' => true, 'no_empty_phpdoc' => true, // 'no_empty_statement' => true, @@ -69,30 +70,13 @@ 'no_spaces_around_offset' => true, // 'no_trailing_comma_in_list_call' => true, // 'no_trailing_comma_in_singleline_array' => true, - // 'no_unneeded_control_parentheses' => true, - // 'no_unused_imports' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, 'no_whitespace_before_comma_in_array' => true, 'no_whitespace_in_blank_line' => true, // 'normalize_index_brace' => true, 'object_operator_without_whitespace' => true, 'php_unit_fqcn_annotation' => true, - 'phpdoc_align' => true, - 'phpdoc_annotation_without_dot' => true, - 'phpdoc_indent' => true, - // 'phpdoc_no_access' => true, - // 'phpdoc_no_alias_tag' => true, - // 'phpdoc_no_empty_return' => true, - // 'phpdoc_no_package' => true, - // 'phpdoc_no_useless_inheritdoc' => true, - // 'phpdoc_return_self_reference' => true, - // 'phpdoc_scalar' => true, - 'phpdoc_separation' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_summary' => true, - // 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - // 'phpdoc_var_without_name' => true, // 'increment_style' => true, // 'return_type_declaration' => true, // 'self_accessor' => true, @@ -106,7 +90,6 @@ 'trim_array_spaces' => true, 'unary_operator_spaces' => true, 'whitespace_after_comma_in_array' => true, - 'space_after_semicolon' => true, 'single_blank_line_at_eof' => true, 'ordered_class_elements' => [ 'order' => [ @@ -165,7 +148,34 @@ ], 'sort_algorithm' => 'alpha' ], - 'ordered_imports' => true + 'ordered_imports' => true, + 'phpdoc_order_by_value' => true, + 'phpdoc_types_order' => ['sort_algorithm' => 'alpha', 'null_adjustment' => 'always_last'], + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + // 'phpdoc_no_access' => true, + // 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => false, + // 'phpdoc_no_package' => true, + // 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_line_span' => ['property' => 'single'], + 'phpdoc_order' => true, + 'phpdoc_param_order' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'no_superfluous_phpdoc_tags' => false + // 'void_return' => true, + // 'phpdoc_var_without_name' => true, ]) ->setIndent(' ') ->setLineEnding("\n") diff --git a/packages/backend-php/app/adapters/v2/EmailAdapter2.php b/packages/backend-php/app/adapters/v2/EmailAdapter2.php new file mode 100644 index 00000000..535e6e5d --- /dev/null +++ b/packages/backend-php/app/adapters/v2/EmailAdapter2.php @@ -0,0 +1,70 @@ +subject; + } + + return \vhs\gateways\Engine::getInstance() + ->getDefaultGateway('messages', 'email') + ->sendRichEmail($recipients, $subject, $generated->txt, $generated->html); + } + + /** + * Send the email. + * + * @param mixed $template + * @param mixed $context + * @param string|null $subject + * + * @throws \app\exceptions\InvalidInputException + * + * @return bool + */ + public function EmailUser(User $user, $template, $context, $subject = null): bool { + $generated = EmailTemplate::generate($template, $context); + + if (is_null($generated)) { + throw new InvalidInputException('Unable to load e-mail template'); + } + + if (is_null($subject)) { + $subject = $generated->subject; + } + + return \vhs\gateways\Engine::getInstance() + ->getDefaultGateway('messages', 'email') + ->sendRichEmail($user->email, $subject, $generated->txt, $generated->html); + } +} diff --git a/app/app.php b/packages/backend-php/app/app.php similarity index 88% rename from app/app.php rename to packages/backend-php/app/app.php index e639f1f1..a6ebe9f9 100644 --- a/app/app.php +++ b/packages/backend-php/app/app.php @@ -10,14 +10,14 @@ require_once '../conf/config.ini.php'; //Debug defined in /conf/config.ini.php -if (DEBUG) { +if (defined('DEBUG')) { error_reporting(E_ALL); ini_set('display_errors', 1); } require_once 'include.php'; -$serverLog = new \vhs\loggers\FileLogger(dirname(__FILE__) . '/../logs/server.log'); +$serverLog = new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/server.log'); \vhs\web\HttpContext::Init(new \vhs\web\HttpServer(new \vhs\web\modules\HttpServerInfoModule('Nomos'), $serverLog)); @@ -27,6 +27,7 @@ \vhs\web\HttpContext::Server()->register(new \vhs\web\modules\HttpExceptionHandlerModule('verbose', $serverLog)); \vhs\web\HttpContext::Server()->register(\app\modules\HttpPaymentGatewayHandlerModule::getInstance()); \vhs\web\HttpContext::Server()->register(\app\security\oauth\modules\OAuthHandlerModule::getInstance()); +\vhs\web\HttpContext::Server()->register(new \vhs\web\modules\HttpJsonServiceHandlerModule('v2')); \vhs\web\HttpContext::Server()->register(new \vhs\web\modules\HttpJsonServiceHandlerModule('web')); \app\modules\HttpPaymentGatewayHandlerModule::register(new \app\gateways\PaypalGateway()); @@ -42,4 +43,6 @@ \app\security\oauth\modules\OAuthHandlerModule::register(new \app\security\oauth\modules\GoogleOAuthHandler()); \app\security\oauth\modules\OAuthHandlerModule::register(new \app\security\oauth\modules\SlackOAuthHandler()); +\vhs\gateways\Engine::getInstance()->setLogger($serverLog); + \vhs\web\HttpContext::Server()->handle(); diff --git a/packages/backend-php/app/constants/Errors.php b/packages/backend-php/app/constants/Errors.php new file mode 100644 index 00000000..131c0c54 --- /dev/null +++ b/packages/backend-php/app/constants/Errors.php @@ -0,0 +1,11 @@ + $context + * @param string|null $subject + * + * @return void + */ + public function Email($email, $tmpl, $context, $subject = null): void; + + /** + * Summary of EmailUser. + * + * @permission administrator + * + * @param \app\domain\User $user email address + * @param string $tmpl + * @param array $context + * @param string|null $subject + * + * @return void + */ + public function EmailUser($user, $tmpl, $context, $subject = null): void; + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\EmailTemplate|null + */ + public function GetTemplate($id): EmailTemplate|null; + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\EmailTemplate[] + */ + public function ListTemplates($page, $size, $columns, $order, $filters): array; + + /** + * @permission administrator + * + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function PutTemplate($name, $code, $subject, $help, $body, $html): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function UpdateTemplate($id, $name, $code, $subject, $help, $body, $html): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $body + * + * @return bool + */ + public function UpdateTemplateBody($id, $body): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $code + * + * @return bool + */ + public function UpdateTemplateCode($id, $code): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $help + * + * @return bool + */ + public function UpdateTemplateHelp($id, $help): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $html + * + * @return bool + */ + public function UpdateTemplateHtml($id, $html): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * + * @return bool + */ + public function UpdateTemplateName($id, $name): bool; + + /** + * @permission administrator + * + * @param int $id + * @param string $subject + * + * @return bool + */ + public function UpdateTemplateSubject($id, $subject): bool; +} diff --git a/packages/backend-php/app/contracts/v2/IEventService2.php b/packages/backend-php/app/contracts/v2/IEventService2.php new file mode 100644 index 00000000..76626996 --- /dev/null +++ b/packages/backend-php/app/contracts/v2/IEventService2.php @@ -0,0 +1,140 @@ +|array + */ + public function GetUserGrantablePrivileges($userid): array; + + /** + * @permission administrator + * + * @return \app\domain\User[] + */ + public function GetUsers(): array; + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @return bool + */ + public function GrantPrivilege($userid, $privilege): bool; + + /** + * @permission administrator|grants + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\User[] + */ + public function ListUsers($page, $size, $columns, $order, $filters): array; + + /** + * @permission administrator + * + * @param int $userid + * @param string|string[] $privileges + * + * @return bool + */ + public function PutUserPrivileges($userid, $privileges): bool; + + /** + * @permission anonymous + * + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * + * @return \app\domain\User + */ + public function Register($username, $password, $email, $fname, $lname): User; + + /** + * @permission anonymous + * + * @param string $email + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function RequestPasswordReset($email): ServiceResponseSuccess|ServiceResponseError; + + /** + * @permission user + * + * @param string $email + * + * @return bool|string|null + */ + public function RequestSlackInvite($email): bool|string|null; + + /** + * @permission anonymous + * + * @param string $token + * @param string $password + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function ResetPassword($token, $password): ServiceResponseSuccess|ServiceResponseError; + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @return bool + */ + public function RevokePrivilege($userid, $privilege): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param bool|string $cash + * + * @return bool + */ + public function UpdateCash($userid, $cash): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateEmail($userid, $email): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param string $date + * + * @return bool + */ + public function UpdateExpiry($userid, $date): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param int $membershipid + * + * @return bool + */ + public function UpdateMembership($userid, $membershipid): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $fname + * @param string $lname + * + * @return bool + */ + public function UpdateName($userid, $fname, $lname): bool; + + /** + * @permission administrator|user + * + * @param int $userid + * @param bool $subscribe + * + * @return bool + */ + public function UpdateNewsletter($userid, $subscribe): bool; + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $password + * + * @return bool + */ + public function UpdatePassword($userid, $password): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdatePaymentEmail($userid, $email): bool; + + /** + * @permission administrator + * + * @param int $userid + * @param string $status + * + * @return bool + */ + public function UpdateStatus($userid, $status): bool; + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateStripeEmail($userid, $email): bool; + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $username + * + * @return bool + */ + public function UpdateUsername($userid, $username): bool; +} diff --git a/packages/backend-php/app/contracts/v2/IWebHookService2.php b/packages/backend-php/app/contracts/v2/IWebHookService2.php new file mode 100644 index 00000000..1d900e2d --- /dev/null +++ b/packages/backend-php/app/contracts/v2/IWebHookService2.php @@ -0,0 +1,152 @@ + + * + * @typescript + */ +class AccessLog extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + AccessLog::Schema(AccessLogSchema::Type()); + } + + /** + * findLatest. + * + * @param int $limit + * + * @return AccessLog[] + */ + public static function findLatest($limit = 5) { + return self::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(AccessLogSchema::Columns()->authorized, false), + // TODO implement proper typing + // @phpstan-ignore property.notFound + OrderBy::Descending(AccessLogSchema::Columns()->time), + $limit + ); + } + + /** + * log. + * + * @param string $key + * @param string $type + * @param bool $authorized + * @param string $from_ip + * @param int|null $userid + * + * @return AccessLog + */ + public static function log($key, $type, $authorized, $from_ip, $userid = null) { + $entry = new AccessLog(); + + $entry->key = $key; + $entry->type = $type; + $entry->authorized = $authorized; + $entry->from_ip = $from_ip; + $entry->time = date(Database::DateFormat()); + $entry->userid = $userid; + $entry->save(); + + return $entry; + } + + /** + * @param ValidationResults $results + * + * @return bool + */ + public function validate(ValidationResults &$results) { + return true; + } +} diff --git a/packages/backend-php/app/domain/AccessToken.php b/packages/backend-php/app/domain/AccessToken.php new file mode 100644 index 00000000..d8963e30 --- /dev/null +++ b/packages/backend-php/app/domain/AccessToken.php @@ -0,0 +1,78 @@ + + * + * @typescript + */ +class AccessToken extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + AccessToken::Schema(AccessTokenSchema::Type()); + AccessToken::Relationship('user', User::Type()); + AccessToken::Relationship('client', AppClient::Type()); + } + + /** + * findByToken. + * + * @param string $token + * + * @return AccessToken|null + */ + public static function findByToken($token) { + $tokens = self::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(AccessTokenSchema::Columns()->token, $token), + // TODO implement proper typing + // @phpstan-ignore property.notFound + OrderBy::Descending(AccessTokenSchema::Columns()->expires), + 1 + ); + + if (count($tokens) === 1) { + return $tokens[0]; + } + + return null; + } + + /** + * @param ValidationResults $results + * + * @return bool + */ + public function validate(ValidationResults &$results) { + return true; + } +} diff --git a/packages/backend-php/app/domain/AppClient.php b/packages/backend-php/app/domain/AppClient.php new file mode 100644 index 00000000..87dc1ab4 --- /dev/null +++ b/packages/backend-php/app/domain/AppClient.php @@ -0,0 +1,55 @@ + + * + * @typescript + */ +class AppClient extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + AppClient::Schema(AppClientSchema::Type()); + AppClient::Relationship('owner', User::Type()); + } + + /** + * @param ValidationResults $results + * + * @return bool + */ + public function validate(ValidationResults &$results) { + return true; + } +} diff --git a/packages/backend-php/app/domain/EmailTemplate.php b/packages/backend-php/app/domain/EmailTemplate.php new file mode 100644 index 00000000..423680cb --- /dev/null +++ b/packages/backend-php/app/domain/EmailTemplate.php @@ -0,0 +1,96 @@ + + * + * @typescript + */ +class EmailTemplate extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + EmailTemplate::Schema(EmailSchema::Type()); + } + + /** + * findByCode. + * + * @param string $code + * + * @return EmailTemplate|null + */ + public static function findByCode($code) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + $val = EmailTemplate::where(Where::Equal(EmailSchema::Columns()->code, $code)); + + if (!empty($val)) { + return $val[0]; + } + + return null; + } + + /** + * generate. + * + * @param string $code + * @param array|string $context + * + * @return \app\dto\GeneratedEmailResults|null + */ + public static function generate($code, $context) { + $template = self::findByCode($code); + + if (is_null($template)) { + return null; + } + + $engine = new \StringTemplate\Engine('{{', '}}'); + + $ret = new GeneratedEmailResults(); + + $ret->subject = $engine->render($template->subject, $context); + $ret->txt = $engine->render($template->body, $context); + $ret->html = $engine->render($template->html, $context); + + return $ret; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Event.php b/packages/backend-php/app/domain/Event.php new file mode 100644 index 00000000..10db5393 --- /dev/null +++ b/packages/backend-php/app/domain/Event.php @@ -0,0 +1,76 @@ + + * + * @typescript + */ +class Event extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Event::Schema(EventSchema::Type()); + + Event::Relationship('privileges', Privilege::Type(), EventPrivilegeSchema::Type()); + } + + /** + * exists. + * + * @param mixed $domain + * @param mixed $event + * + * @return bool + */ + public static function exists($domain, $event) { + $events = Event::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Event::Schema()->Columns()->domain, $domain), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Event::Schema()->Columns()->event, $event) + ) + ); + + return count($events) > 0; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/GenuineCard.php b/packages/backend-php/app/domain/GenuineCard.php new file mode 100644 index 00000000..ea9b3b31 --- /dev/null +++ b/packages/backend-php/app/domain/GenuineCard.php @@ -0,0 +1,63 @@ + + * + * @typescript + */ +class GenuineCard extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + GenuineCard::Schema(GenuineCardSchema::Type()); + } + + /** + * @param string $key + * + * @return GenuineCard[] + */ + public static function findByKey($key) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return GenuineCard::where(Where::Equal(GenuineCardSchema::Columns()->key, $key)); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Ipn.php b/packages/backend-php/app/domain/Ipn.php new file mode 100644 index 00000000..28931fc6 --- /dev/null +++ b/packages/backend-php/app/domain/Ipn.php @@ -0,0 +1,52 @@ + + * + * @typescript + */ +class Ipn extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Ipn::Schema(IpnSchema::Type()); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Key.php b/packages/backend-php/app/domain/Key.php new file mode 100644 index 00000000..81a1be87 --- /dev/null +++ b/packages/backend-php/app/domain/Key.php @@ -0,0 +1,254 @@ + + * + * @typescript + */ +class Key extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Key::Schema(KeySchema::Type()); + + Key::Relationship('privileges', Privilege::Type(), KeyPrivilegeSchema::Type()); + } + + /** + * find key by api key. + * + * @param string $key + * + * @return \app\domain\Key[] + */ + public static function findByApiKey($key) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'api'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $key) + ) + ); + } + + /** + * find key by pin key. + * + * @param string $pin + * + * @return Key[]|null + */ + public static function findByPin($pin) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'pin'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $pin) + ) + ); + } + + /** + * find key by rfid key. + * + * @param string $rfid + * + * @return Key[]|null + */ + public static function findByRfid($rfid) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'rfid'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $rfid) + ) + ); + } + + /** + * find key by service key. + * + * @param string $service + * @param string $key + * + * @return Key[]|null + */ + public static function findByService($service, $key) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, $service), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $key) + ) + ); + } + + /** + * find keys by types. + * + * @param string ...$types + * + * @return Key[]|null + */ + public static function findByTypes(...$types) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return self::where(Where::In(KeySchema::Columns()->type, $types)); + } + + /** + * find by key and type. + * + * @param string $key + * @param string $type + * + * @return Key[]|null + */ + public static function findKeyAndType($key, $type) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, $type), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->key, $key) + ) + ); + } + + /** + * get system (non-owned) api keys. + * + * @return Key[]|null + */ + public static function getSystemApiKeys() { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Null(KeySchema::Columns()->userid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(KeySchema::Columns()->type, 'api') + ) + ); + } + + /** + * get api keys for a particular user id. + * + * @param int $userid + * + * @return Key[]|null + */ + public static function getUserApiKeys($userid) { + return self::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->type, 'api'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->userid, $userid) + ) + ); + } + + /** + * getAbsolutePrivileges. + * + * @return mixed + */ + public function getAbsolutePrivileges() { + $privs = []; + + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + foreach ($this->privileges->all() as $priv) { + if ($priv->code === 'inherit' && $this->userid != null) { + $user = User::find($this->userid); + + if ($user != null) { + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $userpriv) { + array_push($privs, $userpriv); + } + + if (!is_null($user->membership)) { + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + foreach ($user->membership->privileges->all() as $mempriv) { + array_push($privs, $mempriv); + } + } + } + } + + array_push($privs, $priv); + } + + $retval = []; + + foreach (array_unique($privs) as $priv) { + //hack array_unique may convert to object + array_push($retval, $priv); + } + + return $retval; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Membership.php b/packages/backend-php/app/domain/Membership.php new file mode 100644 index 00000000..a7bb1ce4 --- /dev/null +++ b/packages/backend-php/app/domain/Membership.php @@ -0,0 +1,148 @@ + + * + * @typescript + */ +class Membership extends Domain { + public const FRIEND = 'vhs_membership_friend'; + /* TODO HACK we should instead add privileges to the membership types and check those instead when checking types + * however currently the metric service is also using these to determine new member types, etc so we need to figure + * out how to do metrics a bit more dynamically. Prob by querying and looping through all the membership types instead + * or have the client request them. + */ + public const KEYHOLDER = 'vhs_membership_keyholder'; + public const MEMBER = 'vhs_membership_member'; + + /** + * Get a map of all code IDs. + * + * @return array + */ + public static function allCodeIdMap() { + $rows = Database::select( + Query::Select(MembershipSchema::Table(), new Columns(MembershipSchema::Column('id'), MembershipSchema::Column('code'))) + ); + + $values = []; + + foreach ($rows as $row) { + $values['id' . $row['id']] = $row['code']; + } + + return $values; + } + + /** + * Undocumented function. + * + * @return string[] + */ + public static function allCodes() { + $rows = Database::select(Query::Select(MembershipSchema::Table(), new Columns(MembershipSchema::Column('code')))); + + $values = []; + + foreach ($rows as $row) { + array_push($values, $row['code']); + } + + return $values; + } + + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Membership::Schema(MembershipSchema::Type()); + + Membership::Relationship('privileges', Privilege::Type(), MembershipPrivilegeSchema::Type()); + } + + /** + * Find membership by code. + * + * @param string $code + * + * @return Membership[] + */ + public static function findByCode($code) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return Membership::where(Where::Equal(MembershipSchema::Columns()->code, $code)); + } + + /** + * @param float $price + * + * @return Membership|null + */ + public static function findForPriceLevel($price) { + $memberships = Membership::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(MembershipSchema::Columns()->price, $price), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(MembershipSchema::Columns()->active, true) + ), + // TODO implement proper typing + // @phpstan-ignore property.notFound + OrderBy::Descending(MembershipSchema::Columns()->price), + 1 + ); + + if (!is_null($memberships) && !empty($memberships)) { + return $memberships[0]; + } + + return null; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/PasswordResetRequest.php b/packages/backend-php/app/domain/PasswordResetRequest.php new file mode 100644 index 00000000..51049fad --- /dev/null +++ b/packages/backend-php/app/domain/PasswordResetRequest.php @@ -0,0 +1,60 @@ + + * + * @typescript + */ +class PasswordResetRequest extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + PasswordResetRequest::Schema(PasswordResetRequestSchema::Type()); + } + + /** + * findByToken. + * + * @param string $token + * + * @return PasswordResetRequest[]|null + */ + public static function findByToken($token) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return PasswordResetRequest::where(Where::Equal(PasswordResetRequestSchema::Columns()->token, $token)); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Payment.php b/packages/backend-php/app/domain/Payment.php new file mode 100644 index 00000000..9ed9315b --- /dev/null +++ b/packages/backend-php/app/domain/Payment.php @@ -0,0 +1,79 @@ + + * + * @typescript + */ +class Payment extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Payment::Schema(PaymentSchema::Type()); + } + + /** + * exists. + * + * @param string $txn_id + * + * @return bool + */ + public static function exists($txn_id) { + return Database::exists( + Query::select( + PaymentSchema::Table(), + PaymentSchema::Columns(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(PaymentSchema::Columns()->txn_id, $txn_id) + ) + ); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/Privilege.php b/packages/backend-php/app/domain/Privilege.php new file mode 100644 index 00000000..a9c59714 --- /dev/null +++ b/packages/backend-php/app/domain/Privilege.php @@ -0,0 +1,119 @@ + + * + * @typescript + */ +class Privilege extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Privilege::Schema(PrivilegeSchema::Type()); + } + + /** + * findByCode. + * + * @param string $code + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Privilege|null + */ + public static function findByCode($code) { + if (!self::checkCodeAccess($code)) { + throw new UnauthorizedException(); + } + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $privs = Privilege::where(Where::Equal(Privilege::Schema()->Columns()->code, $code)); + + if (!empty($privs)) { + return $privs[0]; + } + + return null; + } + + /** + * findByCodes. + * + * @param string ...$codes + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Privilege[]|null + */ + public static function findByCodes(string ...$codes) { + if (!self::checkCodeAccess(...$codes)) { + throw new UnauthorizedException(); + } + + // TODO implement proper typing + // @phpstan-ignore property.notFound + return Privilege::where(Where::In(Privilege::Schema()->Columns()->code, $codes)); + } + + /** + * checkCodeAccess. + * + * @param string ...$codes + * + * @return bool + */ + private static function checkCodeAccess(string ...$codes) { + foreach ($codes as $code) { + if ( + $code != 'inherit' && + !CurrentUser::hasAllPermissions('administrator') && + !CurrentUser::hasAllPermissions($code) && + !CurrentUser::canGrantAllPermissions($code) + ) { + return false; + } + } + + return true; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/RefreshToken.php b/packages/backend-php/app/domain/RefreshToken.php new file mode 100644 index 00000000..ef2311c6 --- /dev/null +++ b/packages/backend-php/app/domain/RefreshToken.php @@ -0,0 +1,78 @@ + + * + * @typescript + */ +class RefreshToken extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + RefreshToken::Schema(RefreshTokenSchema::Type()); + RefreshToken::Relationship('user', User::Type()); + RefreshToken::Relationship('client', AppClient::Type()); + } + + /** + * findByToken. + * + * @param string $token + * + * @return \app\domain\RefreshToken|null + */ + public static function findByToken($token) { + $tokens = self::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(RefreshTokenSchema::Columns()->token, $token), + // TODO implement proper typing + // @phpstan-ignore property.notFound + OrderBy::Descending(RefreshTokenSchema::Columns()->expires), + 1 + ); + + if (count($tokens) === 1) { + return $tokens[0]; + } + + return null; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return bool + */ + public function validate(ValidationResults &$results) { + return true; + } +} diff --git a/packages/backend-php/app/domain/StripeEvent.php b/packages/backend-php/app/domain/StripeEvent.php new file mode 100644 index 00000000..07fcf6f3 --- /dev/null +++ b/packages/backend-php/app/domain/StripeEvent.php @@ -0,0 +1,50 @@ + + * + * @typescript + */ +class StripeEvent extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + StripeEvent::Schema(StripeEventSchema::Type()); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/SystemPreference.php b/packages/backend-php/app/domain/SystemPreference.php new file mode 100644 index 00000000..1cab7729 --- /dev/null +++ b/packages/backend-php/app/domain/SystemPreference.php @@ -0,0 +1,79 @@ + + * + * @typescript + */ +class SystemPreference extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + SystemPreference::Schema(SystemPreferenceSchema::Type()); + + SystemPreference::Relationship('privileges', Privilege::Type(), SystemPreferencePrivilegeSchema::Type()); + } + + /** + * @param string $key + * @param callable|null $accessCheck Privilege[] returns bool + * + * @return SystemPreference[] + */ + public static function findByKey($key, ?callable $accessCheck = null) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + $prefs = SystemPreference::where(Where::Equal(SystemPreferenceSchema::Columns()->key, $key)); + + if (is_null($prefs) || count($prefs) == 0 || is_null($accessCheck)) { + return $prefs; + } + + $accessiblePrefs = []; + + /** @var SystemPreference $pref */ + foreach ($prefs as $pref) { + if ($accessCheck($pref->privileges)) { + array_push($accessiblePrefs, $pref); + } + } + + return $accessiblePrefs; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/domain/User.php b/packages/backend-php/app/domain/User.php new file mode 100644 index 00000000..d641a6db --- /dev/null +++ b/packages/backend-php/app/domain/User.php @@ -0,0 +1,255 @@ + + * + * @typescript + */ +class User extends Domain { + /** + * Define. + */ + public static function Define(): void { + User::Schema(UserSchema::Type()); + User::Relationship('keys', Key::Type()); + User::Relationship('membership', Membership::Type()); + User::Relationship('privileges', Privilege::Type(), UserPrivilegeSchema::Type()); + } + + /** + * @param string|null $username + * @param string|null $email + * + * @return bool + */ + public static function exists($username = null, $email = null) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + $usernameWhere = Where::Equal(UserSchema::Columns()->username, $username); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $emailWhere = Where::Equal(UserSchema::Columns()->email, $email); + $where = null; + + if (!is_null($username) && !is_null($email)) { + $where = Where::_Or($usernameWhere, $emailWhere); + } elseif (!is_null($email)) { + $where = $emailWhere; + } else { + $where = $usernameWhere; + } + + return Database::exists(Query::select(UserSchema::Table(), UserSchema::Columns(), $where)); + } + + /** + * @param string $email + * + * @return User[]|null + */ + public static function findByEmail($email) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return User::where(Where::Equal(UserSchema::Columns()->email, $email)); + } + + /** + * @param string $email + * + * @return User[]|null + */ + public static function findByPaymentEmail($email) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return User::where(Where::Equal(UserSchema::Columns()->payment_email, $email)); + } + + /** + * findByToken. + * + * @param mixed $token + * + * @return User[]|null + */ + public static function findByToken($token) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return User::where(Where::Equal(UserSchema::Columns()->token, $token)); + } + + /** + * @param string $username + * + * @return User[]|null + */ + public static function findByUsername($username) { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return User::where(Where::Equal(UserSchema::Columns()->username, $username)); + } + + /** + * Magic field interface method for 'valid'. + * + * @return bool + */ + public function get_valid() { + // Check if account is active + if ($this->active != UserActiveEnum::ACTIVE->value) { + return false; + } + + // Check for administrator privilege + // We don't want to accidentally lock out administrators + // TODO: improve this + $privs = $this->getPrivilegeCodes(); + if (in_array('administrator', $privs)) { + return true; + } + + // check if membership has expired + return !$this->hasExpired(); + } + + /** + * getGrantCodes. + * + * @return array + */ + public function getGrantCodes() { + $grants = []; + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($this->privileges->all() as $priv) { + if (strpos($priv->code, 'grant:') === 0) { + array_push($grants, substr($priv->code, 6)); + } + } + + return $grants; + } + + /** + * Get a friendly error message for user validity. + * + * @return 'Account expired'|'Account is not active'|'Unknown error'|false + */ + public function getInvalidReason() { + if ($this->valid) { + return false; + } + + // Check if account is active + if ($this->active != UserActiveEnum::ACTIVE->value) { + return 'Account is not active'; + } + + // check if membership has expired + if ($this->hasExpired()) { + return 'Account expired'; + } + + return 'Unknown error'; + } + + /** + * getPrivilegeCodes. + * + * @return string[] + */ + public function getPrivilegeCodes() { + $codes = []; + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($this->privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + return $codes; + } + + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } + + /** + * Check if user account has expired. + * + * @return bool + */ + private function hasExpired() { + return new DateTime($this->mem_expire) < new DateTime(); + } + + /** + * validateEmail. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + * + * @disregard + */ + // @phpstan-ignore method.unused + private function validateEmail(ValidationResults &$results) { + if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) { + $results->add(new ValidationFailure('Invalid e-mail address')); + } + } +} diff --git a/packages/backend-php/app/domain/WebHook.php b/packages/backend-php/app/domain/WebHook.php new file mode 100644 index 00000000..cc70e32d --- /dev/null +++ b/packages/backend-php/app/domain/WebHook.php @@ -0,0 +1,80 @@ + + * + * @typescript + */ +class WebHook extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + WebHook::Schema(WebHookSchema::Type()); + + WebHook::Relationship('privileges', Privilege::Type(), WebHookPrivilegeSchema::Type()); + WebHook::Relationship('event', Event::Type()); + } + + /** + * findByDomainEvent. + * + * @param string $domain + * @param string $event + * + * @return WebHook[]|null + */ + public static function findByDomainEvent($domain, $event) { + return WebHook::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(WebHook::Schema()->Columns()->domain, $domain), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(WebHook::Schema()->Columns()->event, $event) + ) + ); + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/app/dto/AccessLog.php b/packages/backend-php/app/dto/AccessLog.php new file mode 100644 index 00000000..ca979c82 --- /dev/null +++ b/packages/backend-php/app/dto/AccessLog.php @@ -0,0 +1,33 @@ +id = $accessLog->id; + $this->key = $accessLog->key; + $this->type = $accessLog->type; + $this->authorized = $accessLog->authorized; + $this->from_ip = $accessLog->from_ip; + $this->time = $accessLog->time; + $this->userid = $accessLog->userid; + } +} diff --git a/packages/backend-php/app/dto/AccessToken.php b/packages/backend-php/app/dto/AccessToken.php new file mode 100644 index 00000000..64399b2c --- /dev/null +++ b/packages/backend-php/app/dto/AccessToken.php @@ -0,0 +1,27 @@ +id = $accessToken->id; + $this->token = $accessToken->token; + $this->expires = $accessToken->expires; + $this->userid = $accessToken->userid; + $this->appclientid = $accessToken->appclientid; + } +} diff --git a/packages/backend-php/app/dto/AppClient.php b/packages/backend-php/app/dto/AppClient.php new file mode 100644 index 00000000..5908977f --- /dev/null +++ b/packages/backend-php/app/dto/AppClient.php @@ -0,0 +1,39 @@ +id = $appClient->id; + $this->secret = $appClient->secret; + $this->expires = $appClient->expires; + $this->userid = $appClient->userid; + $this->name = $appClient->name; + $this->description = $appClient->description; + $this->url = $appClient->url; + $this->redirecturi = $appClient->redirecturi; + $this->enabled = $appClient->enabled; + } +} diff --git a/packages/backend-php/app/dto/AppClientInfo.php b/packages/backend-php/app/dto/AppClientInfo.php new file mode 100644 index 00000000..c1523033 --- /dev/null +++ b/packages/backend-php/app/dto/AppClientInfo.php @@ -0,0 +1,25 @@ +name = $clientInfo->name; + $this->description = $clientInfo->description; + $this->redirecturi = $clientInfo->redirecturi; + $this->url = $clientInfo->url; + } +} diff --git a/packages/backend-php/app/dto/EmailTemplate.php b/packages/backend-php/app/dto/EmailTemplate.php new file mode 100644 index 00000000..21614fba --- /dev/null +++ b/packages/backend-php/app/dto/EmailTemplate.php @@ -0,0 +1,33 @@ +id = $emailTemplate->id; + $this->name = $emailTemplate->name; + $this->code = $emailTemplate->code; + $this->subject = $emailTemplate->subject; + $this->help = $emailTemplate->help; + $this->body = $emailTemplate->body; + $this->html = $emailTemplate->html; + } +} diff --git a/packages/backend-php/app/dto/Event.php b/packages/backend-php/app/dto/Event.php new file mode 100644 index 00000000..feed23a2 --- /dev/null +++ b/packages/backend-php/app/dto/Event.php @@ -0,0 +1,30 @@ +id = $event->id; + $this->name = $event->name; + $this->domain = $event->domain; + $this->event = $event->event; + $this->description = $event->description; + $this->enabled = $event->enabled; + } +} diff --git a/packages/backend-php/app/dto/GeneratedEmailResults.php b/packages/backend-php/app/dto/GeneratedEmailResults.php new file mode 100644 index 00000000..7d039501 --- /dev/null +++ b/packages/backend-php/app/dto/GeneratedEmailResults.php @@ -0,0 +1,14 @@ +id = $genuineCard->id; + $this->key = $genuineCard->key; + $this->created = $genuineCard->created; + $this->issued = $genuineCard->issued; + $this->active = $genuineCard->active; + $this->paymentid = $genuineCard->paymentid; + $this->userid = $genuineCard->userid; + $this->owneremail = $genuineCard->owneremail; + $this->notes = $genuineCard->notes; + } +} diff --git a/packages/backend-php/app/dto/Ipn.php b/packages/backend-php/app/dto/Ipn.php new file mode 100644 index 00000000..1fd1865c --- /dev/null +++ b/packages/backend-php/app/dto/Ipn.php @@ -0,0 +1,44 @@ +id = $ipn->id; + $this->created = $ipn->created; + $this->validation = EnumMapper::tryFrom(IpnValidationEnum::cases(), $ipn->validation); + $this->payment_status = $ipn->payment_status; + $this->payment_amount = $ipn->payment_amount; + $this->payment_currency = $ipn->payment_currency; + $this->payer_email = $ipn->payer_email; + $this->item_name = $ipn->item_name; + $this->item_number = $ipn->item_number; + $this->raw = $ipn->raw; + } +} diff --git a/packages/backend-php/app/dto/IpnValidationEnum.php b/packages/backend-php/app/dto/IpnValidationEnum.php new file mode 100644 index 00000000..5139c284 --- /dev/null +++ b/packages/backend-php/app/dto/IpnValidationEnum.php @@ -0,0 +1,13 @@ +id = $key->id; + $this->userid = $key->userid; + $this->type = EnumMapper::tryFrom(KeyTypeEnum::cases(), $key->type); + $this->key = $key->key; + $this->created = $key->created; + $this->notes = $key->notes; + $this->expires = $key->expires; + } +} diff --git a/packages/backend-php/app/dto/KeyTypeEnum.php b/packages/backend-php/app/dto/KeyTypeEnum.php new file mode 100644 index 00000000..768bee08 --- /dev/null +++ b/packages/backend-php/app/dto/KeyTypeEnum.php @@ -0,0 +1,18 @@ +id = $membership->id; + $this->title = $membership->title; + $this->code = $membership->code; + $this->description = $membership->description; + $this->price = $membership->price; + $this->days = $membership->days; + $this->period = $membership->period; + $this->trial = $membership->trial; + $this->recurring = $membership->recurring; + $this->private = $membership->private; + $this->active = $membership->active; + } +} diff --git a/packages/backend-php/app/dto/PasswordResetRequest.php b/packages/backend-php/app/dto/PasswordResetRequest.php new file mode 100644 index 00000000..abaeb9e6 --- /dev/null +++ b/packages/backend-php/app/dto/PasswordResetRequest.php @@ -0,0 +1,24 @@ +id = $passwordResetRequest->id; + $this->userid = $passwordResetRequest->userid; + $this->token = $passwordResetRequest->token; + $this->created = $passwordResetRequest->created; + } +} diff --git a/packages/backend-php/app/dto/Payment.php b/packages/backend-php/app/dto/Payment.php new file mode 100644 index 00000000..722b87f2 --- /dev/null +++ b/packages/backend-php/app/dto/Payment.php @@ -0,0 +1,59 @@ +id = $payment->id; + $this->txn_id = $payment->txn_id; + $this->membership_id = $payment->membership_id; + $this->user_id = $payment->user_id; + $this->payer_email = $payment->payer_email; + $this->payer_fname = $payment->payer_fname; + $this->payer_lname = $payment->payer_lname; + $this->rate_amount = $payment->rate_amount; + $this->currency = $payment->currency; + $this->date = $payment->date; + $this->pp = EnumMapper::tryFrom(PaymentPpEnum::cases(), $payment->pp); + $this->ip = $payment->ip; + $this->status = $payment->status; + $this->item_name = $payment->item_name; + $this->item_number = $payment->item_number; + } +} diff --git a/packages/backend-php/app/dto/PaymentPpEnum.php b/packages/backend-php/app/dto/PaymentPpEnum.php new file mode 100644 index 00000000..e27ebea7 --- /dev/null +++ b/packages/backend-php/app/dto/PaymentPpEnum.php @@ -0,0 +1,14 @@ +id = $privilege->id; + $this->name = $privilege->name; + $this->code = $privilege->code; + $this->description = $privilege->description; + $this->icon = $privilege->icon; + $this->enabled = $privilege->enabled; + } +} diff --git a/packages/backend-php/app/dto/RefreshToken.php b/packages/backend-php/app/dto/RefreshToken.php new file mode 100644 index 00000000..3a4ebbb8 --- /dev/null +++ b/packages/backend-php/app/dto/RefreshToken.php @@ -0,0 +1,27 @@ +id = $refreshToken->id; + $this->token = $refreshToken->token; + $this->expires = $refreshToken->expires; + $this->userid = $refreshToken->userid; + $this->appclientid = $refreshToken->appclientid; + } +} diff --git a/packages/backend-php/app/dto/SavedRefreshToken.php b/packages/backend-php/app/dto/SavedRefreshToken.php new file mode 100644 index 00000000..b96cfc41 --- /dev/null +++ b/packages/backend-php/app/dto/SavedRefreshToken.php @@ -0,0 +1,17 @@ +success = $success; + $this->msg = $msg; + } +} diff --git a/packages/backend-php/app/dto/ServiceResponseError.php b/packages/backend-php/app/dto/ServiceResponseError.php new file mode 100644 index 00000000..7f7b95df --- /dev/null +++ b/packages/backend-php/app/dto/ServiceResponseError.php @@ -0,0 +1,8 @@ +id = $stripeEvent->id; + $this->ts = $stripeEvent->ts; + $this->status = EnumMapper::tryFrom(StripeEventStatusEnum::cases(), $stripeEvent->status); + $this->created = $stripeEvent->created; + $this->event_id = $stripeEvent->event_id; + $this->type = $stripeEvent->type; + $this->object = $stripeEvent->object; + $this->request = $stripeEvent->request; + $this->api_version = $stripeEvent->api_version; + $this->raw = $stripeEvent->raw; + } +} diff --git a/packages/backend-php/app/dto/StripeEventStatusEnum.php b/packages/backend-php/app/dto/StripeEventStatusEnum.php new file mode 100644 index 00000000..96f34b5d --- /dev/null +++ b/packages/backend-php/app/dto/StripeEventStatusEnum.php @@ -0,0 +1,13 @@ +id = $systemPreference->id; + $this->key = $systemPreference->key; + $this->value = $systemPreference->value; + $this->enabled = $systemPreference->enabled; + $this->notes = $systemPreference->notes; + } +} diff --git a/packages/backend-php/app/dto/TotalKeyHoldersResult.php b/packages/backend-php/app/dto/TotalKeyHoldersResult.php new file mode 100644 index 00000000..64dc3958 --- /dev/null +++ b/packages/backend-php/app/dto/TotalKeyHoldersResult.php @@ -0,0 +1,7 @@ +id = $clientInfo->id; + $this->name = $clientInfo->name; + $this->description = $clientInfo->description; + $this->enabled = $clientInfo->enabled; + $this->expires = $clientInfo->expires; + $this->owner = new TrimmedUser($clientInfo->owner); + } +} diff --git a/packages/backend-php/app/dto/TrimmedAppClientOwner.php b/packages/backend-php/app/dto/TrimmedAppClientOwner.php new file mode 100644 index 00000000..cc56278f --- /dev/null +++ b/packages/backend-php/app/dto/TrimmedAppClientOwner.php @@ -0,0 +1,35 @@ +id = $userInfo->id; + $this->username = $userInfo->username; + $this->email = $userInfo->email; + $this->fname = $userInfo->fname; + $this->lname = $userInfo->lname; + $this->membership = $userInfo->membership; + $this->created = $userInfo->created; + $this->active = $userInfo->active; + $this->privileges = $userInfo->privileges; + } +} diff --git a/packages/backend-php/app/dto/TrimmedUser.php b/packages/backend-php/app/dto/TrimmedUser.php new file mode 100644 index 00000000..a0b5535b --- /dev/null +++ b/packages/backend-php/app/dto/TrimmedUser.php @@ -0,0 +1,35 @@ +id = $userInfo->id; + $this->username = $userInfo->username; + $this->email = $userInfo->email; + $this->fname = $userInfo->fname; + $this->lname = $userInfo->lname; + $this->membership = $userInfo->membership; + $this->created = $userInfo->created; + $this->active = $userInfo->active; + $this->privileges = $userInfo->privileges; + } +} diff --git a/packages/backend-php/app/dto/User.php b/packages/backend-php/app/dto/User.php new file mode 100644 index 00000000..1acaa140 --- /dev/null +++ b/packages/backend-php/app/dto/User.php @@ -0,0 +1,84 @@ +id = $user->id; + $this->username = $user->username; + $this->password = $user->password; + $this->membership_id = $user->membership_id; + $this->mem_expire = $user->mem_expire; + $this->trial_used = $user->trial_used; + $this->email = $user->email; + $this->fname = $user->fname; + $this->lname = $user->lname; + $this->token = $user->token; + $this->cookie_id = $user->cookie_id; + $this->newsletter = $user->newsletter; + $this->cash = $user->cash; + $this->userlevel = $user->userlevel; + $this->notes = $user->notes; + $this->created = $user->created; + $this->lastlogin = $user->lastlogin; + $this->lastip = $user->lastip; + $this->avatar = $user->avatar; + $this->active = $user->active; + $this->paypal_id = $user->paypal_id; + $this->payment_email = $user->payment_email; + $this->stripe_id = $user->stripe_id; + $this->stripe_email = $user->stripe_email; + } +} diff --git a/packages/backend-php/app/dto/UserActiveEnum.php b/packages/backend-php/app/dto/UserActiveEnum.php new file mode 100644 index 00000000..66185744 --- /dev/null +++ b/packages/backend-php/app/dto/UserActiveEnum.php @@ -0,0 +1,15 @@ +id = $userPrincipal['id']; + $this->permissions = $userPrincipal['permissions']; + } +} diff --git a/packages/backend-php/app/dto/WebHook.php b/packages/backend-php/app/dto/WebHook.php new file mode 100644 index 00000000..80e11635 --- /dev/null +++ b/packages/backend-php/app/dto/WebHook.php @@ -0,0 +1,42 @@ +id = $webHook->id; + $this->name = $webHook->name; + $this->description = $webHook->description; + $this->enabled = $webHook->enabled; + $this->userid = $webHook->userid; + $this->url = $webHook->url; + $this->translation = $webHook->translation; + $this->headers = $webHook->headers; + $this->method = $webHook->method; + $this->eventid = $webHook->eventid; + } +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceGetCreatedDatesResult.php b/packages/backend-php/app/dto/v2/MetricServiceGetCreatedDatesResult.php new file mode 100644 index 00000000..554f4ed3 --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceGetCreatedDatesResult.php @@ -0,0 +1,18 @@ +> $byDowHour + * @property array<"1"|"10"|"11"|"12"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9",array<"0"|"1"|"2"|"3"|"4"|"5"|"6"|"total",int>> $byMonthDow + * + * @typescript + */ +class MetricServiceGetCreatedDatesResult extends DTO { +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceGetMembersResult.php b/packages/backend-php/app/dto/v2/MetricServiceGetMembersResult.php new file mode 100644 index 00000000..d5046362 --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceGetMembersResult.php @@ -0,0 +1,53 @@ + $created + * @property array $expired + * @property array $total + * + * @typescript + */ +class MetricServiceGetMembersResult extends DTO implements IDTO { + /** + * jsonSerialize. + * + * @return mixed + */ + public function jsonSerialize(): mixed { + return $this->__serialize(); + } + + /** + * __serialize. + * + * @return array{start_range: string, end_range: string, created: non-empty-array|stdClass, expired: non-empty-array|stdClass, total: non-empty-array|stdClass} + */ + public function __serialize(): array { + return [ + 'start_range' => $this->start_range, + 'end_range' => $this->end_range, + 'created' => !empty($this->created) ? $this->created : new stdClass(), + 'expired' => count(value: $this->expired) !== 0 ? $this->expired : new stdClass(), + 'total' => !empty($this->total) ? $this->total : new stdClass() + ]; + } + + /** + * __toString. + * + * @return string + */ + public function __toString(): string { + return json_encode($this->jsonSerialize()); + } +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceGetRevenueResult.php b/packages/backend-php/app/dto/v2/MetricServiceGetRevenueResult.php new file mode 100644 index 00000000..c13b939a --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceGetRevenueResult.php @@ -0,0 +1,18 @@ +|int>|string $grouping + * @property array<\app\enums\MetricServiceGroupType,array> $by_membership + * + * @typescript + */ +class MetricServiceGetRevenueResult extends DTO { +} diff --git a/packages/backend-php/app/dto/v2/MetricServiceNewKeyholdersResult.php b/packages/backend-php/app/dto/v2/MetricServiceNewKeyholdersResult.php new file mode 100644 index 00000000..ee442138 --- /dev/null +++ b/packages/backend-php/app/dto/v2/MetricServiceNewKeyholdersResult.php @@ -0,0 +1,15 @@ +validation; } + /** + * Name. + * + * @return string + */ public function Name() { return 'paypal'; } + /** + * Process. + * + * @param mixed $data + * + * @throws \app\gateways\PaymentGatewayException + * + * @return string + */ public function Process($data) { // Put this url into paypal // IPN URL: http://cook.vanhack.ca:8888/services/gateways/paypal @@ -59,6 +94,7 @@ public function Process($data) { // Step 2: POST IPN data back to PayPal to validate // ToDo: this should be an option available from the admin interface $paypal = 'https://ipnpb.paypal.com/cgi-bin/webscr'; + // @phpstan-ignore if.alwaysFalse if (DEBUG) { $paypal = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr'; } @@ -66,12 +102,12 @@ public function Process($data) { $ch = curl_init($paypal); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $req); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_FORBID_REUSE, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); $error = null; @@ -103,10 +139,12 @@ public function Process($data) { } elseif (strcmp($response, 'INVALID') == 0) { // IPN invalid, log for manual investigation $result = $this->CreateInvalidIPNRecord($req); + throw new PaymentGatewayException('Error: Could not validate paypal IPN ' . $req); } $this->CreateInvalidIPNRecord($req); + throw new PaymentGatewayException('Error: Unknown Paypal IPN Error ' . $req); } } diff --git a/packages/backend-php/app/gateways/StripeGateway.php b/packages/backend-php/app/gateways/StripeGateway.php new file mode 100644 index 00000000..c34bfa25 --- /dev/null +++ b/packages/backend-php/app/gateways/StripeGateway.php @@ -0,0 +1,125 @@ +status = $status; + $stripe_event->created = $created; + $stripe_event->event_id = $event_id; + $stripe_event->type = $type; + $stripe_event->object = $object; + $stripe_event->request = $request; + $stripe_event->api_version = $api_version; + $stripe_event->raw = $raw; + + return $stripe_event; + } + + /** + * Name. + * + * @return string + */ + public function Name() { + return 'stripe'; + } + + /** + * Process. + * + * @param mixed $payload + * + * @throws \app\gateways\PaymentGatewayException + * + * @return mixed + */ + public function Process($payload) { + if (!isset($_SERVER['HTTP_STRIPE_SIGNATURE'])) { + http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); + + throw new PaymentGatewayException('Error: Unknown Stripe Event Error: Missing signature'); + // return; + } + + $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE']; + $event = null; + + try { + $event = \Stripe\Webhook::constructEvent($payload, $sig_header, STRIPE_WEBHOOK_SECRET); + + // Set up the initial event + $event_record = $this->CreateStripeEventRecord( + 'UNKNOWN', + $event->created, + $event->id, + $event->type, + $event->object, + json_encode($event->request), + $event->api_version, + $payload + ); + + // Handle the event + switch ($event->type) { + case 'invoice.paid': + // @phpstan-ignore property.notFound + $paymentIntent = $event->data->object; // contains a StripePaymentIntent + $event_record->status = 'VALID'; + $event_record->save(); + + break; + default: + $event_record->save(); + + throw new PaymentGatewayException('Received unknown event type ' . $event->type); + } + + http_response_code(200); + + return json_encode(['result' => 'OK']); + } catch (\UnexpectedValueException $e) { + // Invalid payload + http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); + + return new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); + } catch (\Stripe\Exception\SignatureVerificationException $e) { + // Invalid signature + http_response_code(HttpStatusCodes::Client_Error_Bad_Request->value); + + return new PaymentGatewayException('Error: Unknown Stripe Event Error ' . $payload); + } + } +} diff --git a/packages/backend-php/app/handlers/v2/ApiKeyServiceHandler2.php b/packages/backend-php/app/handlers/v2/ApiKeyServiceHandler2.php new file mode 100644 index 00000000..c867cbcd --- /dev/null +++ b/packages/backend-php/app/handlers/v2/ApiKeyServiceHandler2.php @@ -0,0 +1,219 @@ +userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $key->delete(); + } + + /** + * @permission administrator + * + * @param string $notes + * + * @return \app\domain\Key + */ + public function GenerateSystemApiKey($notes): Key { + $apiKey = new Key(); + + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); + $apiKey->type = 'api'; + $apiKey->notes = $notes; + + $apiKey->save(); + + return $apiKey; + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $notes + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key + */ + public function GenerateUserApiKey($userid, $notes): Key { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $user = User::find($userid); + + if (is_null($user)) { + throw new InvalidInputException('Invalid userid'); + } + + $apiKey = new Key(); + + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); + $apiKey->type = 'api'; + $apiKey->notes = $notes; + + $user->keys->add($apiKey); + + $user->save(); + + return $apiKey; + } + + /** + * @permission administrator|user + * + * @param int $keyid + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key + */ + public function GetApiKey($keyid): Key { + /** @var \app\domain\Key|null $key */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException(\app\constants\Errors::E_INVALID_KEY_INPUT); + } + + if (!CurrentUser::hasAnyPermissions('administrator')) { + if (is_null($key->userid) || $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + } + + return $key; + } + + /** + * @permission administrator + * + * @return \app\domain\Key[] + */ + public function GetSystemApiKeys(): array { + return Key::getSystemApiKeys(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key[] + */ + public function GetUserApiKeys($userid): array { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + return Key::getUserApiKeys($userid); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string|string[] $privileges + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function PutApiKeyPrivileges($keyid, $privileges): bool { + /** @var \app\domain\Key|null */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException(\app\constants\Errors::E_INVALID_KEY_INPUT); + } + + if (!CurrentUser::hasAnyPermissions('administrator')) { + if (is_null($key->userid) || $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + } + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($key->privileges->all() as $priv) { + $key->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $key->privileges->add($priv); + } + + return $key->save(); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string $notes + * @param string|null $expires + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function UpdateApiKey($keyid, $notes, $expires): bool { + /** @var \app\domain\Key */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException(\app\constants\Errors::E_INVALID_KEY_INPUT); + } + + if (!CurrentUser::hasAnyPermissions('administrator') && (is_null($key->userid) || $key->userid != CurrentUser::getIdentity())) { + throw new UnauthorizedException(); + } + + $key->notes = $notes; + $key->expires = $expires; + + return $key->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/AuthServiceHandler2.php b/packages/backend-php/app/handlers/v2/AuthServiceHandler2.php new file mode 100644 index 00000000..8394a302 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/AuthServiceHandler2.php @@ -0,0 +1,471 @@ +valid) { + $retval->valid = true; + $retval->userId = $user->id; + $retval->username = $user->username; + $retval->type = $user->membership->code; + $retval->privileges = $key->getAbsolutePrivileges(); + + return true; + } elseif (!is_null($user) && $user instanceof User) { + $retval->username = $user->username; + $retval->message = $user->getInvalidReason(); + } else { + $retval->username = 'unknown'; + $retval->message = 'Null user'; + } + + return false; + } + + /** + * Check to see if the user pin and account is valid. + * + * @permission administrator|pin-auth + * + * @param string $pin + * + * @return \app\utils\AuthCheckResult + */ + public function CheckPin($pin): AuthCheckResult { + // pin magic + // TODO: documentation + $pin = str_replace('|', '', $pin); + + $intpin = intval($pin); + + $pinid = intval($intpin / 10000); + + $pin = $intpin - $pinid * 10000; + + $pinid = sprintf('%04s', $pinid); + $pin = sprintf('%04s', $pin); + + // Set defaults + $retval = new AuthCheckResult(); + + // Find key by pin + /** @var \app\domain\Key[] */ + $keys = Key::findByPin($pinid . '|' . $pin); + + $logAccess = function ($granted, $userid = null) use ($pinid, $pin) { + try { + AccessLog::log($pinid . '|' . $pin, 'pin', $granted, $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN', $userid); + } catch (\Exception $ex) { + /*mmm*/ + } + }; + + // If we get an invalid result, log and fail (we should always only get one result) + if (count($keys) != 1) { + $logAccess(false); + + return $retval; + } + + // Get key + $key = $keys[0]; + + // If missing userid, log and fail + if ($key->userid == null) { + $logAccess(false); + + return $retval; + } + + // Fetch userinfo + $user = User::find($key->userid); + + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); + + return $retval; + } + + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + + // Return + return $retval; + } + + /** + * @permission administrator|rfid-auth + * + * @param string $rfid + * + * @return \app\utils\AuthCheckResult + */ + public function CheckRfid($rfid): AuthCheckResult { + // Set defaults + $retval = new AuthCheckResult(); + + // Find key by RFID card id + $keys = Key::findByRfid($rfid); + + $logAccess = function ($granted, $userid = null) use ($rfid) { + try { + AccessLog::log('rfid', $rfid, $granted, $_SERVER['REMOTE_ADDR'], $userid); + } catch (\Exception $ex) { + /*mmm*/ + } + }; + + // If we get an invalid result, log and fail (we should always only get one result) + if (count($keys) != 1) { + $logAccess(false); + + return $retval; + } + + // Fetch key + $key = $keys[0]; + + // Check if there's a userid attached to the key, else fail + if ($key->userid == null) { + $logAccess(false); + + return $retval; + } + + // Fetch userinfo + $user = User::find($key->userid); + + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); + + return $retval; + } + + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + + // Return + return $retval; + } + + /** + * Check to see if the user service/id is valid. A service could be github/slack/google. + * + * @permission administrator|service-auth + * + * @param string $service + * @param string $id + * + * @return \app\utils\AuthCheckResult + */ + public function CheckService($service, $id): AuthCheckResult { + // Set defaults + $retval = new AuthCheckResult(); + + // Always parse service names as lowercase + $service = strtolower($service); + + // Find service key + $keys = Key::findByService($service, $id); + + $logAccess = function ($granted, $userid = null) use ($service, $id) { + try { + AccessLog::log($id, $service, $granted, $_SERVER['REMOTE_ADDR'], $userid); + } catch (\Exception $ex) { + /*mmm*/ + } + }; + + // If we get an invalid result, log and fail (we should always only get one result) + if (count($keys) != 1) { + $logAccess(false); + + return $retval; + } + + // Fetch key + $key = $keys[0]; + + // Check if there's a userid attached to the key, else fail + if ($key->userid == null) { + $logAccess(false); + + return $retval; + } + + // Fetch userinfo + $user = User::find($key->userid); + + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); + + return $retval; + } + + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + + // Return + return $retval; + } + + /** + * @permission anonymous + * + * @param string $username + * + * @return bool + */ + public function CheckUsername($username): bool { + return Database::exists( + new QuerySelect(UserSchema::Table(), UserSchema::Column('username'), Where::Equal(UserSchema::Column('username'), $username)) + ); + } + + /** + * @permission administrator + * + * @param string|\vhs\domain\Filter|null $filters + * + * @return int + */ + public function CountAccessLog($filters): int { + return AccessLog::count($filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param \vhs\domain\Filter|null $filters + * + * @return int + */ + public function CountUserAccessLog($userid, $filters): int { + $filters = $this->addUserIDToFilters($userid, $filters); + + return AccessLog::count($filters); + } + + /** + * @permission anonymous + * + * @return \app\security\UserPrincipal|\vhs\security\AnonPrincipal|\vhs\security\IPrincipal + */ + public function CurrentUser(): IPrincipal|UserPrincipal|AnonPrincipal { + return CurrentUser::getPrincipal(); + } + + /** + * @permission oauth-provider + * + * @param string $username + * @param string $password + * + * @return \app\dto\TrimmedUser|null + */ + public function GetUser($username, $password): TrimmedUser|null { + return $this->trimUser(Authenticate::authenticateOnly($username, $password)); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return \app\domain\AccessLog[] + */ + public function ListAccessLog($page, $size, $columns, $order, $filters): array { + return AccessLog::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\AccessLog[] + */ + public function ListUserAccessLog($userid, $page, $size, $columns, $order, $filters): array { + $filters = $this->addUserIDToFilters($userid, $filters); + + return AccessLog::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission anonymous + * + * @param string $username + * @param string $password + * + * @return string + */ + public function Login($username, $password): string { + try { + Authenticate::getInstance()->login(new UserPassCredentials($username, $password)); + } catch (\Exception $ex) { + return $ex->getMessage(); + } + + return StringLiterals::AUTH_ACCESS_GRANTED; + } + + /** + * @permission user + * + * @return void + */ + public function Logout(): void { + Authenticate::getInstance()->logout(); + } + + /** + * @permission anonymous + * + * @param string $pin + * + * @throws \vhs\exceptions\HttpException + * + * @return string + */ + public function PinLogin($pin): string { + try { + Authenticate::getInstance()->login(new PinCredentials($pin)); + } catch (\Exception $ex) { + throw new HttpException(StringLiterals::AUTH_ACCESS_DENIED, HttpStatusCodes::Client_Error_Forbidden); + } + + return StringLiterals::AUTH_ACCESS_GRANTED; + } + + /** + * @permission anonymous + * + * @param string $key + * + * @throws \vhs\exceptions\HttpException + * + * @return string + */ + public function RfidLogin($key): string { + try { + Authenticate::getInstance()->login(new PinCredentials($key)); + } catch (\Exception $ex) { + throw new HttpException(StringLiterals::AUTH_ACCESS_DENIED, HttpStatusCodes::Client_Error_Forbidden); + } + + return StringLiterals::AUTH_ACCESS_GRANTED; + } + + /** + * Summary of AddUserIDToFilters. + * + * @param mixed $userid + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \vhs\domain\Filter + */ + private function addUserIDToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } + + /** + * Summary of trimUser. + * + * @param \app\domain\User|null $user + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedUser + */ + private function trimUser($user): TrimmedUser { + if (is_null($user)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedUser($user); + } +} diff --git a/packages/backend-php/app/handlers/v2/EmailServiceHandler2.php b/packages/backend-php/app/handlers/v2/EmailServiceHandler2.php new file mode 100644 index 00000000..5e6db037 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/EmailServiceHandler2.php @@ -0,0 +1,266 @@ +delete(); + } + + /** + * Summary of Email. + * + * @permission administrator + * + * @param string $email + * @param string $tmpl + * @param array $context + * @param string|null $subject + * + * @return void + */ + public function Email($email, $tmpl, $context, $subject = null): void { + EmailAdapter2::getInstance()->Email($email, $tmpl, $context, $subject); + } + + /** + * Summary of EmailUser. + * + * @permission administrator + * + * @param \app\domain\User $user email address + * @param string $tmpl + * @param array $context + * @param string|null $subject + * + * @return void + */ + public function EmailUser($user, $tmpl, $context, $subject = null): void { + EmailAdapter2::getInstance()->EmailUser($user, $tmpl, $context, $subject); + } + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\EmailTemplate|null + */ + public function GetTemplate($id): EmailTemplate|null { + /** @var \app\domain\EmailTemplate|null */ + return EmailTemplate::find($id); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\EmailTemplate[] + */ + public function ListTemplates($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\EmailTemplate[] */ + return EmailTemplate::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function PutTemplate($name, $code, $subject, $help, $body, $html): bool { + /** @var EmailTemplate|null */ + $template = EmailTemplate::findByCode($code); + + if (is_null($template)) { + $template = new EmailTemplate(); + } + + $template->name = $name; + $template->code = $code; + $template->subject = $subject; + $template->help = $help; + $template->body = $body; + $template->html = $html; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return bool + */ + public function UpdateTemplate($id, $name, $code, $subject, $help, $body, $html): bool { + /** @var EmailTemplate|null */ + $template = EmailTemplate::find($id); + + if (is_null($template)) { + throw new HttpException('Invalid or missing template id.', HttpStatusCodes::Client_Error_Not_Found); + } + + $template->name = $name; + $template->code = $code; + $template->subject = $subject; + $template->help = $help; + $template->body = $body; + $template->html = $html; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $body + * + * @return bool + */ + public function UpdateTemplateBody($id, $body): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->body = $body; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $code + * + * @return bool + */ + public function UpdateTemplateCode($id, $code): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->code = $code; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $help + * + * @return bool + */ + public function UpdateTemplateHelp($id, $help): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->help = $help; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $html + * + * @return bool + */ + public function UpdateTemplateHtml($id, $html): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->html = $html; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * + * @return bool + */ + public function UpdateTemplateName($id, $name): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->name = $name; + + return $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $subject + * + * @return bool + */ + public function UpdateTemplateSubject($id, $subject): bool { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->subject = $subject; + + return $template->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/EventServiceHandler2.php b/packages/backend-php/app/handlers/v2/EventServiceHandler2.php new file mode 100644 index 00000000..26167745 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/EventServiceHandler2.php @@ -0,0 +1,276 @@ +name = $name; + $evt->domain = $domain; + $evt->event = $event; + $evt->description = $description; + $evt->enabled = $enabled; + + $evt->save(); + + return $evt; + } + + /** + * @permission administrator + * + * @param int $id + * + * @return void + */ + public function DeleteEvent($id): void { + $event = $this->getEventById($id); + + $event->delete(); + } + + /** + * @permission administrator + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function EnableEvent($id, $enabled): bool { + $evt = $this->getEventById($id); + + $evt->enabled = $enabled; + + return $evt->save(); + } + + /** + * @permission user + * + * @return \app\domain\Event[] + */ + public function GetAccessibleEvents(): array { + $events = Event::findAll(); + + if (CurrentUser::hasAllPermissions('administrator')) { + return $events; + } + + $retval = []; + + foreach ($events as $event) { + $privs = []; + foreach ($event->privileges->all() as $priv) { + array_push($privs, $priv->code); + } + + if (CurrentUser::hasAllPermissions(...$privs)) { + array_push($retval, $event); + } + } + + return $retval; + } + + /** + * @permission webhook|administrator + * + * @param string $domain + * + * @return void + */ + public function GetDomainDefinition($domain): void { + // TODO: Implement GetDomainDefinition() method. + } + + /** + * @permission webhook|administrator + * + * @return mixed + */ + public function GetDomainDefinitions(): mixed { + foreach (glob('domain/*.php') as $filename) { + include_once $filename; + } + + $domains = []; + + foreach (get_declared_classes() as $class) { + if (is_subclass_of($class, '\\vhs\\domain\\Domain')) { + $name = str_replace('app\\domain\\', '', $class); + $domains[$name] = [ + 'checks' => $class::Schema()->Table()->checks + ]; + } + } + + return $domains; + } + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\Event + */ + public function GetEvent($id): Event { + return $this->getEventById($id); + } + + /** + * @permission webhook|administrator + * + * @return \app\domain\Event[] + */ + public function GetEvents(): array { + return Event::findAll(); + } + + /** + * @permission administrator + * + * @return string[] + */ + public function GetEventTypes(): array { + $updateKeys = array_filter(get_class_methods('vhs\domain\Domain'), function ($k) { + return preg_match('/^onAny/', $k); + }); + + sort($updateKeys); + + return array_map(fn ($method): string => str_replace('before', 'before:', strtolower(str_replace('onAny', '', $method))), $updateKeys); + } + + /** + * @permission webhook|administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Event[] + */ + public function ListEvents($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\Event[] */ + return Event::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutEventPrivileges($id, $privileges): bool { + $evt = $this->getEventById($id); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($evt->privileges->all() as $priv) { + $evt->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $evt->privileges->add($priv); + } + + return $evt->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * @param string $domain + * @param string $event + * @param string $description + * @param bool $enabled + * + * @return bool + */ + public function UpdateEvent($id, $name, $domain, $event, $description, $enabled): bool { + $evt = $this->getEventById($id); + + $evt->name = $name; + $evt->domain = $domain; + $evt->event = $event; + $evt->description = $description; + $evt->enabled = $enabled; + + return $evt->save(); + } + + /** + * Summary of getEventById. + * + * @param int $id + * + * @throws \app\exceptions\InvalidInputException + * + * @return \app\domain\Event + */ + private function getEventById($id): Event { + /** @var \app\domain\Event|null */ + $evt = Event::find($id); + + if (is_null($evt)) { + throw new InvalidInputException(Errors::E_INVALID_EVENT); + } + + return $evt; + } +} diff --git a/packages/backend-php/app/handlers/v2/IpnServiceHandler2.php b/packages/backend-php/app/handlers/v2/IpnServiceHandler2.php new file mode 100644 index 00000000..b5889f12 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/IpnServiceHandler2.php @@ -0,0 +1,67 @@ +getKeyById($id); + + $key->delete(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $type + * @param string $value + * @param string $notes + * + * @throws \app\exceptions\InvalidInputException + * + * @return \app\domain\Key|null + */ + public function GenerateUserKey($userid, $type, $value, $notes): Key|null { + if (CurrentUser::getIdentity() == $userid || CurrentUser::hasAnyPermissions('administrator')) { + $user = User::find($userid); + + if ($user != null) { + $key = new Key(); + + switch ($type) { + case 'rfid': + $key->key = $value; + + break; + case 'pin': + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid) + ); + $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + /** @disregard P1006 override */ + $key->privileges->add(Privilege::findByCode('inherit')); + + break; + case 'api': + $key->key = bin2hex(openssl_random_pseudo_bytes(32)); + + break; + default: + throw new InvalidInputException('Unsupported key type'); + } + + $key->notes = $notes; + $key->type = $type; + $key->userid = $userid; + + $key->save(); + + return $key; + } + } + + return null; + } + + /** + * @permission administrator + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key[] + */ + public function GetAllKeys(): array { + if (!CurrentUser::hasAnyPermissions('administrator')) { + throw new UnauthorizedException(); + } + + return Key::findAll(); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * + * @return \app\domain\Key + */ + public function GetKey($keyid): Key { + return $this->getKeyById($keyid); + } + + /** + * @permission administrator + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key[] + */ + public function GetSystemKeys(): array { + if (!CurrentUser::hasAnyPermissions('administrator')) { + throw new UnauthorizedException(); + } + + // TODO implement proper typing + // @phpstan-ignore property.notFound + return Key::where(Where::Null(Key::Schema()->Columns()->userid)); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string[] $types + * + * @return \app\domain\Key[] + */ + public function GetUserKeys($userid, $types): array { + if (CurrentUser::getIdentity() == $userid || CurrentUser::hasAnyPermissions('administrator')) { + $user = User::find($userid); + if ($user != null) { + $keys = []; + foreach ($user->keys->all() as $key) { + if (in_array($key->type, $types)) { + array_push($keys, $key); + } + } + + return $keys; + } + } + + return []; + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string|string[] $privileges + * + * @return bool + */ + public function PutKeyPrivileges($keyid, $privileges): bool { + $key = $this->getKeyById($keyid); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($key->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $key->privileges->remove($priv); + } + + foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $key->privileges->add($priv); + } + + return $key->save(); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string $notes + * @param string $expires + * + * @return bool + */ + public function UpdateKey($keyid, $notes, $expires): bool { + $key = $this->getKeyById($keyid); + + $key->notes = $notes; + $key->expires = $expires; + + return $key->save(); + } + + /** + * Summary of getKeyById. + * + * @param mixed $keyid + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key + */ + private function getKeyById($keyid): Key { + /** @var \app\domain\Key|null */ + $key = Key::find($keyid); + + if (is_null($key)) { + throw new InvalidInputException('Invalid keyid'); + } + + if (!CurrentUser::hasAnyPermissions('administrator')) { + if (is_null($key->userid) || $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + } + + return $key; + } +} diff --git a/packages/backend-php/app/handlers/v2/MemberCardServiceHandler2.php b/packages/backend-php/app/handlers/v2/MemberCardServiceHandler2.php new file mode 100644 index 00000000..5a39ebc8 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/MemberCardServiceHandler2.php @@ -0,0 +1,289 @@ +addUserIDToFilters($userid, $filters); + + return GenuineCard::count($filters); + } + + /** + * @permission administrator + * + * @param string $key + * + * @return \app\domain\GenuineCard + */ + public function GetGenuineCardDetails($key): GenuineCard { + return GenuineCard::findByKey($key)[0]; + } + + /** + * @permission administrator + * + * @param string $email + * @param string $key + * + * @throws \app\exceptions\InvalidInputException + * @throws \app\exceptions\MemberCardException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\GenuineCard + */ + public function IssueCard($email, $key): GenuineCard { + $users = User::findByPaymentEmail($email); + + if (is_null($users) || count($users) != 1) { + throw new InvalidInputException('Invalid email address'); + } + + if (!$this->ValidateGenuineCard($key)) { + throw new InvalidInputException('Invalid card'); + } + + $user = $users[0]; + $card = GenuineCard::findByKey($key)[0]; + + $payments = Payment::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound + // TODO eventually put these into card campaigns or something + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->payer_email, $email), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->user_id, $user->id), + Where::NotIn( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Payment::Schema()->Columns()->id, + Query::Select( + GenuineCard::Schema()->Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + new Columns(GenuineCard::Schema()->Columns()->paymentid) + ) + ) + ) + ); + + if (is_null($payments) || count($payments) < 1) { + throw new MemberCardException('User has not paid for a member card.', HttpStatusCodes::Client_Error_Payment_Required); + } + + $payment = $payments[0]; + + $card->paymentid = $payment->id; + $card->active = true; + $card->userid = $user->id; + $card->owneremail = $email; + $card->issued = date('Y-m-d H:i:s'); + $card->notes = 'Issued by admin to ' . $user->fname . ' ' . $user->lname; + + $card->save(); + + $keyService2 = new KeyServiceHandler2(); + + $keyService2->GenerateUserKey($user->id, 'rfid', $key, 'Genuine VHS Membership Card'); + + return $card; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\GenuineCard[] + */ + public function ListGenuineCards($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\GenuineCard[] */ + return GenuineCard::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\GenuineCard[] + */ + public function ListUserGenuineCards($userid, $page, $size, $columns, $order, $filters): array { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::_Or(Filter::Equal('userid', $user->id), Filter::Equal('owneremail', $user->email)); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + /** @var \app\domain\GenuineCard[] */ + return GenuineCard::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $key + * @param string $notes + * + * @throws \app\exceptions\MemberCardException + * + * @return \app\domain\GenuineCard + */ + public function RegisterGenuineCard($key, $notes): GenuineCard { + $keys = GenuineCard::findByKey($key); + + if (!is_null($keys) && count($keys) != 0) { + //card already registered + throw new MemberCardException('Failed to register card'); + } + + $card = new GenuineCard(); + + $card->key = $key; + + $card->save(); + + return $card; + } + + /** + * @permission administrator + * + * @param string $key + * @param bool $active + * + * @throws \app\exceptions\InvalidInputException + * + * @return bool + */ + public function UpdateGenuineCardActive($key, $active): bool { + if (!$this->ValidateGenuineCard($key)) { + throw new InvalidInputException('Invalid card'); + } + + /** @var GenuineCard */ + $card = GenuineCard::findByKey($key)[0]; + + $card->active = $active; + + return $card->save(); + } + + /** + * @permission authenticated + * + * @param string $key + * + * @return bool + */ + public function ValidateGenuineCard($key): bool { + $keys = GenuineCard::findByKey($key); + + return !is_null($keys) && count($keys) == 1; + } + + /** + * addUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ + private function addUserIDToFilters($userid, $filters) { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } +} diff --git a/packages/backend-php/app/handlers/v2/MembershipServiceHandler2.php b/packages/backend-php/app/handlers/v2/MembershipServiceHandler2.php new file mode 100644 index 00000000..73335950 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/MembershipServiceHandler2.php @@ -0,0 +1,221 @@ +getMembershipById($membershipId); + } + + /** + * @permission administrator + * + * @return \app\domain\Membership[] + */ + public function GetAll(): array { + return Membership::findAll(); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Membership[] + */ + public function ListMemberships($page, $size, $columns, $order, $filters): array { + /** @var \app\domain\Membership[] */ + return Membership::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param string|string[] $privileges + * + * @return bool + */ + public function PutPrivileges($membershipId, $privileges): bool { + $membership = $this->getMembershipById($membershipId); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + foreach ($membership->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + $membership->privileges->remove($priv); + } + + foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 PHP0404 override */ + $membership->privileges->add($priv); + } + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param string $title + * @param string $description + * @param int $price + * @param string $code + * @param int $days + * @param string $period + * + * @return bool + */ + public function Update($membershipId, $title, $description, $price, $code, $days, $period): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->title = $title; + $membership->description = $description; + $membership->price = $price; + $membership->code = $code; + $membership->days = $days; + $membership->period = $period; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $active + * + * @return bool + */ + public function UpdateActive($membershipId, $active): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->active = $active; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $privateVal + * + * @return bool + */ + public function UpdatePrivate($membershipId, $privateVal): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->private = $privateVal; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $recurring + * + * @return bool + */ + public function UpdateRecurring($membershipId, $recurring): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->recurring = $recurring; + + return $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $trial + * + * @return bool + */ + public function UpdateTrial($membershipId, $trial): bool { + $membership = $this->getMembershipById($membershipId); + + $membership->trial = $trial; + + return $membership->save(); + } + + /** + * getMembershipById. + * + * @param int $membershipId + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return Membership + */ + private function getMembershipById($membershipId): Membership { + /** @var Membership|null */ + $membership = Membership::find($membershipId); + + if (is_null($membership)) { + throw new DomainException(sprintf('Missing membership for queried id: [%s]', $membershipId)); + } + + return $membership; + } +} diff --git a/packages/backend-php/app/handlers/v2/MetricServiceHandler2.php b/packages/backend-php/app/handlers/v2/MetricServiceHandler2.php new file mode 100644 index 00000000..5cf8d64b --- /dev/null +++ b/packages/backend-php/app/handlers/v2/MetricServiceHandler2.php @@ -0,0 +1,485 @@ +Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(User::Schema()->Columns()->created, $end_range) + ) + ); + + $byDowHour = []; + + $byMonthDow = []; + + foreach ($users as $user) { + $date = new DateTime($user->created); + + $dow = $date->format('w'); + $hour = $date->format('G'); + $month = $date->format('n'); + + if (!array_key_exists($dow, $byDowHour)) { + $byDowHour[$dow] = []; + $byDowHour[$dow]['total'] = 0; + } + + $byDowHour[$dow]['total'] += 1; + + if (!array_key_exists($hour, $byDowHour[$dow])) { + $byDowHour[$dow][$hour] = 0; + } + + $byDowHour[$dow][$hour] += 1; + + if (!array_key_exists($month, $byMonthDow)) { + $byMonthDow[$month] = []; + $byMonthDow[$month]['total'] = 0; + } + + $byMonthDow[$month]['total'] += 1; + + if (!array_key_exists($dow, $byMonthDow[$month])) { + $byMonthDow[$month][$dow] = 0; + } + + $byMonthDow[$month][$dow] += 1; + } + + $result = new MetricServiceGetCreatedDatesResult([ + 'start_range' => $start_range, + 'end_range' => $end_range, + 'byDowHour' => $byDowHour, + 'byMonthDow' => $byMonthDow + ]); + + return $result; + } + + /** + * @permission administrator + * + * @return \app\domain\Payment[] + */ + public function GetExceptionPayments(): array { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return Payment::where(Where::NotEqual(Payment::Schema()->Columns()->status, 1)); + } + + /** + * @permission user + * + * @param string $start_range + * @param string $end_range + * @param \app\enums\MetricServiceGroupType $group + * + * @return \app\dto\v2\MetricServiceGetMembersResult + */ + public function GetMembers($start_range, $end_range, $group): MetricServiceGetMembersResult { + $users = User::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(User::Schema()->Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(User::Schema()->Columns()->created, $end_range) + ) + ); + + $payments = Payment::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Like(Payment::Schema()->Columns()->item_number, 'vhs_membership_%') + ) + ); + + $created = []; + $expired = []; + + foreach ($users as $user) { + $created[] = $this->countByDate($created, $user->created, $group); + $expired[] = $this->countByDate($expired, $user->mem_expire, $group); + } + + $total = []; + + foreach ($payments as $payment) { + $total = $this->countByDate($total, $payment->date, $group); + } + + ksort($created); + ksort($expired); + ksort($total); + + $result = new MetricServiceGetMembersResult([ + 'start_range' => $start_range, + 'end_range' => $end_range, + 'created' => $created, + 'expired' => $expired, + 'total' => $total + ]); + + return $result; + } + + /** + * @permission user + * + * @param string $start_range string iso date in UTC, if empty is start of today + * @param string $end_range string iso date in UTC, if empty is end of today + * + * @return \app\dto\v2\MetricServiceNewKeyholdersResult + */ + public function GetNewKeyHolders($start_range, $end_range): MetricServiceNewKeyholdersResult { + $start = strtotime($start_range); + $end = strtotime($end_range); + $membership = Membership::findByCode(Membership::KEYHOLDER); + $count = $this->NewMembershipByIdCount($membership[0]->id, $start, $end); + + return new MetricServiceNewKeyholdersResult([ + 'value' => $count + ]); + } + + /** + * @permission user + * + * @param string $start_range string iso date in UTC, if empty is start of today + * @param string $end_range string iso date in UTC, if empty is end of today + * + * @return \app\dto\v2\MetricServiceNewMembersResult + */ + public function GetNewMembers($start_range, $end_range): MetricServiceNewMembersResult { + $start = strtotime($start_range); + $end = strtotime($end_range); + $count = $this->NewMemberCount($start, $end); + + return new MetricServiceNewMembersResult([ + 'start_range' => $start_range, + 'start' => $start, + 'end_range' => $end_range, + 'end' => $end, + 'value' => $count + ]); + } + + /** + * @permission administrator + * + * @return \app\domain\User[] + */ + public function GetPendingAccounts(): mixed { + // TODO implement proper typing + // @phpstan-ignore property.notFound + return User::where(Where::Equal(User::Schema()->Columns()->active, UserActiveEnum::PENDING->value)); + } + + /** + * @permission user + * + * @param string $start_range string iso date in UTC, if empty is end of today + * @param string $end_range string iso date in UTC, if empty is end of today + * @param \app\enums\MetricServiceGroupType $group group by month, day, year + * + * @return \app\dto\v2\MetricServiceGetRevenueResult + */ + public function GetRevenue($start_range, $end_range, $group): MetricServiceGetRevenueResult { + $payments = Payment::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range) + ) + ); + + $byMembership = []; + $byDate = []; + + foreach ($payments as $payment) { + $membershipKey = $payment->item_number == '' || is_null($payment->item_number) ? 'Donation' : $payment->item_number; + if (!array_key_exists($membershipKey, $byMembership)) { + $byMembership[$membershipKey] = []; + } + + $grouping = new DateTime($payment->date); + + switch ($group) { + case 'day': + $grouping = $grouping->format('Y-m-d'); + + break; + case 'month': + $grouping = $grouping->format('Y-m'); + + break; + case 'year': + $grouping = $grouping->format('Y'); + + break; + default: + $grouping = 'all'; + + break; + } + + if (!array_key_exists($grouping, $byMembership[$membershipKey])) { + $byMembership[$membershipKey][$grouping] = 0; + } + + $byMembership[$membershipKey][$grouping] += $payment->rate_amount; + + if (!array_key_exists($grouping, $byDate)) { + $byDate[$grouping] = 0; + } + + $byDate[$grouping] += $payment->rate_amount; + } + + return new MetricServiceGetRevenueResult([ + 'start_range' => $start_range, + 'end_range' => $end_range, + 'grouping' => $byDate, + 'by_membership' => $byMembership + ]); + } + + /** + * @permission user + * + * @return \app\dto\v2\MetricServiceTotalKeyHoldersResult + */ + public function GetTotalKeyHolders(): MetricServiceTotalKeyHoldersResult { + $membership = Membership::findByCode(Membership::KEYHOLDER); + $count = $this->TotalMembershipByIdCount($membership[0]->id); + + return new MetricServiceTotalKeyHoldersResult([ + 'value' => $count + ]); + } + + /** + * @permission user + * + * @return \app\dto\v2\MetricServiceTotalMembersResult + */ + public function GetTotalMembers(): MetricServiceTotalMembersResult { + $count = $this->TotalMemberCount(); + + return new MetricServiceTotalMembersResult([ + 'value' => $count + ]); + } + + /** + * countByDate. + * + * @param mixed[] $arr + * @param mixed $date + * @param mixed $group + * + * @return mixed[] + */ + private function countByDate($arr, $date, $group): mixed { + if (is_null($date)) { + return $arr; + } + + $grouping = new DateTime($date); + + if ($grouping > new DateTime()) { + return $arr; + } + + switch ($group) { + case 'day': + $grouping = $grouping->format('Y-m-d'); + + break; + case 'month': + $grouping = $grouping->format('Y-m'); + + break; + case 'year': + $grouping = $grouping->format('Y'); + + break; + default: + $grouping = 'all'; + + break; + } + + if (!array_key_exists($grouping, $arr)) { + $arr[$grouping] = 0; + } + + $arr[$grouping] += 1; + + return $arr; + } + + /** + * Get the total new members recorded in the date range. + * + * @param int $start int unixtime + * @param int $end int unixtime + * + * @return int + */ + private function NewMemberCount($start, $end): int { + $query = Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $start)) + ) + ); + + return Database::count($query); + } + + /** + * Get the total new memberships of a type recorded in the date range. + * + * @param int $membership_id int + * @param int $start int unixtime + * @param int $end int unixtime + * + * @return int + */ + private function NewMembershipByIdCount($membership_id, $start, $end): int { + $query = Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->membership_id, $membership_id), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::LesserEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->created, date(Formats::DATE_TIME_ISO_SHORT_MIDNIGHT, $start)) + ) + ); + + return Database::count($query); + } + + /** + * Get the total members. + * + * @return int + */ + private function TotalMemberCount(): int { + return Database::count( + Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)) + ) + ) + ); + } + + /** + * TotalMembershipByIdCount. + * + * @param int $membership_id + * + * @return int + */ + private function TotalMembershipByIdCount($membership_id): int { + return Database::count( + Query::count( + UserSchema::Table(), + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->active, UserActiveEnum::ACTIVE->value), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::GreaterEqual(UserSchema::Columns()->mem_expire, date(Formats::DATE_TIME_ISO_SHORT_FULL)), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(UserSchema::Columns()->membership_id, $membership_id) + ) + ) + ); + } +} diff --git a/packages/backend-php/app/handlers/v2/OAuthServiceHandler2.php b/packages/backend-php/app/handlers/v2/OAuthServiceHandler2.php new file mode 100644 index 00000000..737bdbc6 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/OAuthServiceHandler2.php @@ -0,0 +1,485 @@ +addUserIDToFilters($userid, $filters); + + return AppClient::count($filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return void + */ + public function DeleteClient($id): void { + $client = $this->getOAuthClient($id); + + $client->delete(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function EnableClient($id, $enabled): bool { + $client = $this->getOAuthClient($id); + + $client->enabled = $enabled; + + return $client->save(); + } + + /** + * @permission oauth-provider + * + * @param string $bearerToken + * + * @return \app\domain\AccessToken + */ + public function GetAccessToken($bearerToken): AccessToken { + return AccessToken::findByToken($bearerToken); + } + + /** + * @permission anonymous + * + * @param int $clientId + * @param string $clientSecret + * + * @return \app\dto\TrimmedAppClient|null + */ + public function GetClient($clientId, $clientSecret): TrimmedAppClient|null { + $client = AppClient::find($clientId); + + if (!is_null($client) && $client->secret == $clientSecret) { + return $this->trimClient($client); + } + + return null; + } + + /** + * GetClientDetails. + * + * @permission administrator|user + * + * @param int $id + * + * @return \app\domain\AppClient + */ + public function GetClientDetails($id): AppClient { + return $this->getOAuthClient($id); + } + + /** + * @permission oauth-provider + * @permission authenticated + * + * @param int $clientId + * + * @return \app\dto\TrimmedAppClient|null + */ + public function GetClientInfo($clientId): TrimmedAppClient|null { + $client = AppClient::find($clientId); + + if (!is_null($client)) { + return $this->trimClientInfo($client); + } + + return null; + } + + /** + * @permission oauth-provider + * + * @param string $refreshToken + * + * @return \app\domain\RefreshToken + */ + public function GetRefreshToken($refreshToken): RefreshToken { + return RefreshToken::findByToken($refreshToken); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\AppClient[] + */ + public function ListClients($page, $size, $columns, $order, $filters): array { + return AppClient::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\AppClient[] + */ + public function ListUserClients($userid, $page, $size, $columns, $order, $filters): array { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + AppClient::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + $cols = explode(',', $columns); + + array_push($cols, 'userid'); + + $columns = implode(',', array_unique($cols)); + + return AppClient::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param string $name + * @param string $description + * @param string $url + * @param string $redirecturi + * + * @return \app\domain\AppClient + */ + public function RegisterClient($name, $description, $url, $redirecturi): AppClient { + $client = new AppClient(); + + $client->name = $name; + $client->description = $description; + $client->url = $url; + $client->redirecturi = $redirecturi; + $client->secret = bin2hex(openssl_random_pseudo_bytes(32)); + $client->owner = User::find(CurrentUser::getIdentity()); + $client->expires = date('Y-m-d', strtotime('+1 year')); + + if (!$client->save()) { + throw new HttpException('Failed to save new client!', HttpStatusCodes::Server_Error_Internal_Service_Error); + } + + return $client; + } + + /** + * @permission oauth-provider + * + * @param string $refreshToken + * + * @throws \vhs\exceptions\HttpException + * + * @return void + */ + public function RevokeRefreshToken($refreshToken): void { + $token = RefreshToken::findByToken($refreshToken); + + if (is_null($token)) { + throw new HttpException('RefreshToken token not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $token->delete(); + } + + /** + * @permission oauth-provider + * + * @param int $userId + * @param string $accessToken + * @param int $clientId + * @param string $expires + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\domain\User|false + */ + public function SaveAccessToken($userId, $accessToken, $clientId, $expires): User|false { + $user = User::find($userId); + + if (is_null($user)) { + throw new HttpException('User not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $token = new AccessToken(); + + $token->token = $accessToken; + $token->user = $user; + + $client = AppClient::find($clientId); + + if (!is_null($client)) { + $token->client = $client; + } + + $expiry = new DateTime($expires); + + $token->expires = $expiry->format('Y-m-d H:i:s'); + + $token->save(); + + return $user; + } + + /** + * @permission oauth-provider + * + * @param int $userId + * @param string $refreshToken + * @param int $clientId + * @param string $expires + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedUser + */ + public function SaveRefreshToken($userId, $refreshToken, $clientId, $expires): TrimmedUser { + $user = User::find($userId); + $client = AppClient::find($clientId); + + if (is_null($user)) { + throw new HttpException('User not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $token = new RefreshToken(); + + $token->token = $refreshToken; + $token->user = $user; + + if (!is_null($client)) { + $token->client = $client; + } + + $expiry = new DateTime($expires); + + $token->expires = $expiry->format('Y-m-d H:i:s'); + + $token->save(); + + return $this->trimUser($user); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string $name + * @param string $description + * @param string $url + * @param string $redirecturi + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateClient($id, $name, $description, $url, $redirecturi): bool { + $client = $this->getOAuthClient($id); + + $client->name = $name; + $client->description = $description; + $client->url = $url; + $client->redirecturi = $redirecturi; + + return $client->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $expires + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateClientExpiry($id, $expires): bool { + $client = AppClient::find($id); + + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $client->expires = $expires; + + return $client->save(); + } + + /** + * Summary of AddUserIDToFilters. + * + * @param mixed $userid + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \vhs\domain\Filter + */ + private function addUserIDToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } + + /** + * getOAuthClient. + * + * @param int $id + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\domain\AppClient + */ + private function getOAuthClient($id): AppClient { + $client = AppClient::find($id); + + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + if (CurrentUser::getIdentity() != $client->userid && !CurrentUser::hasAnyPermissions('administrator')) { + throw new HttpException('Client is not accessible', HttpStatusCodes::Client_Error_Forbidden); + } + + return $client; + } + + /** + * Summary of trimClient. + * + * @param \app\domain\AppClient $client + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedAppClient + */ + private function trimClient($client): TrimmedAppClient { + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedAppClient($client); + } + + /** + * Summary of trimClientInfo. + * + * @param \app\domain\AppClient|null $client + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedAppClient + */ + private function trimClientInfo($client): TrimmedAppClient { + if (is_null($client)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedAppClient($client); + } + + /** + * Summary of trimUser. + * + * @param \app\domain\User|null $user + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\dto\TrimmedUser + */ + private function trimUser($user): TrimmedUser { + if (is_null($user)) { + throw new HttpException('Client not found', HttpStatusCodes::Client_Error_Not_Found); + } + + return new TrimmedUser($user); + } +} diff --git a/packages/backend-php/app/handlers/v2/PaymentServiceHandler2.php b/packages/backend-php/app/handlers/v2/PaymentServiceHandler2.php new file mode 100644 index 00000000..3ea4f784 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PaymentServiceHandler2.php @@ -0,0 +1,154 @@ +addUserIDOrEMailToFilters($userid, $filters); + + return Payment::count($filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return \app\domain\Payment|null + */ + public function GetPayment($id): Payment|null { + /** @var Payment|null */ + $payment = Payment::find($id); + + if (is_null($payment)) { + return null; + } + + if (CurrentUser::getIdentity() == $payment->user_id || CurrentUser::hasAnyPermissions('administrator')) { + return $payment; + } + + return null; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Payment[] + */ + public function ListPayments($page, $size, $columns, $order, $filters): array { + return Payment::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Payment[] + */ + public function ListUserPayments($userid, $page, $size, $columns, $order, $filters): array { + $filters = $this->addUserIDOrEMailToFilters($userid, $filters); + + return Payment::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $paymentid + * + * @return string + */ + public function ReplayPaymentProcessing($paymentid): string { + $log = new StringLogger(); + + $log->log('Attempting a reply of payment id: ' . $paymentid); + + $processor = new PaymentProcessor($log); + + try { + $processor->paymentCreated($paymentid); + } catch (\Exception $ex) { + $log->log('Exception: ' . $ex->getMessage()); + $log->log($ex->getTraceAsString()); + } + + $log->log('Replay complete.'); + + // @phpstan-ignore method.notFound + return $log->fullText(); + } + + /** + * addUserIDOrEMailToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ + private function addUserIDOrEMailToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::_Or(Filter::Equal('user_id', $user->id), Filter::Equal('payer_email', $user->email)); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } +} diff --git a/packages/backend-php/app/handlers/v2/PinServiceHandler2.php b/packages/backend-php/app/handlers/v2/PinServiceHandler2.php new file mode 100644 index 00000000..5f345079 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PinServiceHandler2.php @@ -0,0 +1,227 @@ +getUserPinByUserId($userid); + + if (is_null($pin)) { + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid)) + ); + + $key = new Key(); + $key->userid = $userid; + $key->type = 'pin'; + $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + $key->notes = 'User generated PIN'; + + $pin = $key; + + $priv = Privilege::findByCode('inherit'); + if (!is_null($priv)) { + // TODO fix typing + /** @disregard P1006 override */ + $pin->privileges->add($priv); + } + } + + $pinid = explode('|', $pin->key)[0]; + + $pin->key = sprintf('%04s', $pinid) . '|' . sprintf('%04s', rand(0, 9999)); + $pin->notes = 'User generated PIN'; + + $pin->save(); + + return $pin; + } + + /** + * @permission gen-temp-pin|administrator + * + * @param string $expires + * @param string $privileges + * @param string $notes + * + * @return \app\domain\Key + */ + public function GenerateTemporaryPin($expires, $privileges, $notes): Key { + $userid = CurrentUser::getIdentity(); + + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid)) + ); + + $pin = new Key(); + $pin->userid = $userid; + $pin->expires = $expires; + $pin->type = 'pin'; + $pin->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + $pin->notes = $notes; + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + if (!is_null($privs) && is_array($privs)) { + foreach ($privs as $priv) { + if (CurrentUser::hasAllPermissions($priv->code)) { + // TODO fix typing + /** @disregard P1006 override */ + $pin->privileges->add($priv); + } + } + } + + $pin->save(); + + return $pin; + } + + /** + * @permission administrator|user + * + * @param int $userid + * + * @return \app\domain\Key|null + */ + public function GetUserPin($userid): Key|null { + return $this->getUserPinByUserId($userid); + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string $pin + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function UpdatePin($keyid, $pin): bool { + /** @var \app\domain\Key */ + $key = Key::find($keyid); + + if (!CurrentUser::hasAnyPermissions('administrator') && $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $pinid = explode('|', $key->key)[0]; + + $key->key = $pinid . '|' . sprintf('%04s', intval($pin)); + + return $key->save(); + } + + /** + * Change a pin. + * + * @permission administrator|user + * + * @param int $userid + * @param string $pin + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function UpdateUserPin($userid, $pin): bool { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $pinObj = $this->getUserPinByUserId($userid); + + if (is_null($pin)) { + $pinObj = $this->GeneratePin($userid); + } + + $pinid = explode('|', $pinObj->key)[0]; + + $pinObj->key = $pinid . '|' . $pin; + + return $pinObj->save(); + } + + /** + * Summary of getUserPinByUserId. + * + * @param mixed $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key|null + */ + private function getUserPinByUserId($userid): ?Key { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $keys = Key::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->type, 'pin'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->userid, $userid) + ) + ); + + return !empty($keys) ? $keys[0] : null; + } +} diff --git a/packages/backend-php/app/handlers/v2/PreferenceServiceHandler2.php b/packages/backend-php/app/handlers/v2/PreferenceServiceHandler2.php new file mode 100644 index 00000000..c781e733 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PreferenceServiceHandler2.php @@ -0,0 +1,240 @@ +delete(); + } + } + + /** + * @permission administrator + * + * @return \app\domain\SystemPreference[] + */ + public function GetAllSystemPreferences(): array { + return SystemPreference::findAll(); + } + + /** + * @permission administrator + * + * @param int $id + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return \app\domain\SystemPreference + */ + public function GetSystemPreference($id): SystemPreference { + /** @var SystemPreference|null */ + $systemPreference = SystemPreference::find($id); + + if (is_null($systemPreference)) { + throw new DomainException(sprintf('SystemPreference with id [%s] not found!', $id)); + } + + return $systemPreference; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\SystemPreference[] + */ + public function ListSystemPreferences($page, $size, $columns, $order, $filters): array { + /** @var SystemPreference[] */ + return SystemPreference::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @return \app\domain\SystemPreference + */ + public function PutSystemPreference($key, $value, $enabled, $notes): SystemPreference { + $prefs = SystemPreference::findByKey($key); + + $pref = null; + + if (count($prefs) != 1) { + $pref = new SystemPreference(); + } else { + $pref = $prefs[0]; + } + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + $pref->save(); + + return $pref; + } + + /** + * @permission administrator + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutSystemPreferencePrivileges($id, $privileges): bool { + /** @var \app\domain\SystemPreference */ + $pref = SystemPreference::find($id); + + if (is_null($pref)) { + return false; + } + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($pref->privileges->all() as $priv) { + $pref->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $pref->privileges->add($priv); + } + + return $pref->save(); + } + + /** + * @permission anonymous + * + * @param string $key + * + * @return \app\domain\SystemPreference|null + */ + public function SystemPreference($key): SystemPreference|null { + $prefs = SystemPreference::findByKey($key, function ($privileges) { + $codes = []; + foreach ($privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + return CurrentUser::hasAllPermissions(...$codes); + }); + + if (count($prefs) != 1) { + return null; + } + + return $prefs[0]; + } + + /** + * @permission administrator + * + * @param int $id + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateSystemPreference($id, $key, $value, $enabled, $notes): bool { + /** @var \app\domain\SystemPreference */ + $pref = SystemPreference::find($id); + + if (is_null($pref)) { + throw new HttpException('SystemPreference not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + return $pref->save(); + } + + /** + * @permission administrator + * + * @param string $key + * @param bool $enabled + * + * @throws \vhs\exceptions\HttpException + * + * @return bool + */ + public function UpdateSystemPreferenceEnabled($key, $enabled): bool { + /** @var \app\domain\SystemPreference[] */ + $prefs = SystemPreference::findByKey($key); + + $pref = null; + + if (count($prefs) != 1) { + throw new HttpException('SystemPreference not found', HttpStatusCodes::Client_Error_Not_Found); + } + + $pref = $prefs[0]; + + $pref->key = $key; + $pref->enabled = $enabled; + + return $pref->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/PrivilegeServiceHandler2.php b/packages/backend-php/app/handlers/v2/PrivilegeServiceHandler2.php new file mode 100644 index 00000000..97b76dd2 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/PrivilegeServiceHandler2.php @@ -0,0 +1,252 @@ +name = $name; + $priv->code = $code; + $priv->description = $description; + $priv->icon = $icon; + $priv->enabled = $enabled; + + $priv->save(); + + return $priv; + } + + /** + * @permission administrator + * + * @param int $id + * + * @return void + */ + public function DeletePrivilege($id): void { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->delete(); + } + + /** + * @permission administrator|user|grants + * + * @return \app\domain\Privilege[] + */ + public function GetAllPrivileges(): array { + /** @var \app\domain\Privilege[] */ + return Privilege::findAll(); + } + + /** + * @permission administrator + * + * @return string[] + */ + public function GetAllSystemPermissions(): array { + $endpoints = ServiceRegistry::get('v2')->getAllEndpoints(); + + $flatPerms = []; + + /** @var Endpoint $endpoint */ + foreach ($endpoints as $endpoint) { + foreach ($endpoint->getAllPermissions() as $permissions) { + foreach ($permissions as $set) { + array_push($flatPerms, ...$set); + } + } + } + + $flatPerms = array_unique($flatPerms); + + $retval = []; + + foreach ($flatPerms as $perm) { + array_push($retval, $perm); + } + + return $retval; + } + + /** + * @permission user + * + * @param int $id + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return \app\domain\Privilege + */ + public function GetPrivilege($id): Privilege { + /** @var \app\domain\Privilege */ + $privilege = Privilege::find($id); + + if (is_null($privilege)) { + throw new DomainException(sprintf('Privilege with id [%s] not found!', $id), HttpStatusCodes::Client_Error_Not_Found->value); + } + + return $privilege; + } + + /** + * @permission administrator|user|grants + * + * @param int $userid + * + * @return \app\domain\Privilege[] + */ + public function GetUserPrivileges($userid): array { + $privileges = []; + $userService2 = new UserServiceHandler2($this->context); + + $user = $userService2->GetUser($userid); + + if (!is_null($user)) { + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $privilege) { + array_push($privileges, $privilege); + } + + foreach ($user->membership->privileges->all() as $privilege) { + array_push($privileges, $privilege); + } + } + + return $privileges; + } + + /** + * @permission administrator|user|grants + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\Privilege[] + */ + public function ListPrivileges($page, $size, $columns, $order, $filters): array { + /** @var Privilege[] */ + return Privilege::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $description + * + * @return bool + */ + public function UpdatePrivilegeDescription($id, $description): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->description = $description; + + return $priv->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function UpdatePrivilegeEnabled($id, $enabled): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->enabled = $enabled; + + return $priv->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $icon + * + * @return bool + */ + public function UpdatePrivilegeIcon($id, $icon): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->icon = $icon; + + return $priv->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * + * @return bool + */ + public function UpdatePrivilegeName($id, $name): bool { + /** @var \app\domain\Privilege */ + $priv = Privilege::find($id); + + $priv->name = $name; + + return $priv->save(); + } +} diff --git a/packages/backend-php/app/handlers/v2/StripeEventServiceHandler2.php b/packages/backend-php/app/handlers/v2/StripeEventServiceHandler2.php new file mode 100644 index 00000000..9454e69c --- /dev/null +++ b/packages/backend-php/app/handlers/v2/StripeEventServiceHandler2.php @@ -0,0 +1,72 @@ +value); + } + + return $stripeEvent; + } + + /** + * @permission administrator + * + * @return \app\domain\StripeEvent[] + */ + public function GetAll(): array { + return StripeEvent::findAll(); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\StripeEvent[] + */ + public function ListRecords($page, $size, $columns, $order, $filters): array { + return StripeEvent::page($page, $size, $columns, $order, $filters); + } +} diff --git a/packages/backend-php/app/handlers/v2/SystemPreferenceServiceHandler2.php b/packages/backend-php/app/handlers/v2/SystemPreferenceServiceHandler2.php new file mode 100644 index 00000000..b533a500 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/SystemPreferenceServiceHandler2.php @@ -0,0 +1,260 @@ +getSystemPreferencesByKey($keys); + + if (count($prefs) === 0) { + $this->throwNotFound(); + } + + foreach ($prefs as $pref) { + $pref->delete(); + } + } + + /** + * @permission administrator + * + * @return \app\domain\SystemPreference[] + */ + public function GetAllSystemPreferences(): array { + return SystemPreference::findAll(); + } + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\SystemPreference + */ + public function GetSystemPreference($id): SystemPreference { + return $this->getSystemPreferenceById($id); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\SystemPreference[] + */ + public function ListSystemPreferences($page, $size, $columns, $order, $filters): array { + return SystemPreference::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @return \app\domain\SystemPreference + */ + public function PutSystemPreference($key, $value, $enabled, $notes): SystemPreference { + $pref = null; + + try { + $pref = $this->getSystemPreferenceByKey($key); + } catch (\Exception $err) { + $pref = new SystemPreference(); + } + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + $pref->save(); + + return $pref; + } + + /** + * @permission administrator + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutSystemPreferencePrivileges($id, $privileges): bool { + $pref = $this->getSystemPreferenceById($id); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($pref->privileges->all() as $priv) { + $pref->privileges->remove($priv); + } + + foreach ($privs as $priv) { + $pref->privileges->add($priv); + } + + return $pref->save(); + } + + /** + * @permission anonymous + * + * @param string $key + * + * @throws \vhs\exceptions\HttpException + * + * @return \app\domain\SystemPreference + */ + public function SystemPreference($key): SystemPreference { + $prefs = SystemPreference::findByKey($key, function ($privileges) { + $codes = []; + foreach ($privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + return CurrentUser::hasAllPermissions(...$codes); + }); + + if (count($prefs) != 1) { + throw new HttpException('Invalid SystemPreference result', HttpStatusCodes::Client_Error_Not_Found); + } + + return $prefs[0]; + } + + /** + * @permission administrator + * + * @param int $id + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes + * + * @return bool + */ + public function UpdateSystemPreference($id, $key, $value, $enabled, $notes): bool { + $pref = $this->getSystemPreferenceById($id); + + $pref->key = $key; + $pref->value = $value; + $pref->enabled = $enabled; + $pref->notes = $notes; + + return $pref->save(); + } + + /** + * @permission administrator + * + * @param string $key + * @param bool $enabled + * + * @return bool + */ + public function UpdateSystemPreferenceEnabled($key, $enabled): bool { + $pref = $this->getSystemPreferenceByKey($key); + + $pref->key = $key; + $pref->enabled = $enabled; + + return $pref->save(); + } + + /** + * getSystemPreferenceById. + * + * @param int $id + * + * @return \app\domain\SystemPreference + */ + private function getSystemPreferenceById($id): SystemPreference { + /** @var SystemPreference|null */ + $pref = SystemPreference::find($id); + + if (is_null($pref)) { + $this->throwNotFound(); + } + + return $pref; + } + + /** + * getSystemPreferencesByKey. + * + * @param string $key + * + * @return \app\domain\SystemPreference + */ + private function getSystemPreferenceByKey($key): SystemPreference { + /** @var SystemPreference[]|null */ + $prefs = SystemPreference::findByKey($key); + + if (is_null($prefs) || count($prefs) !== 1) { + $this->throwNotFound(); + } + + return $prefs[0]; + } + + /** + * getSystemPreferencesByKey. + * + * @param string ...$keys + * + * @return \app\domain\SystemPreference[] + */ + private function getSystemPreferencesByKey(string ...$keys): array { + /** @var SystemPreference[]|null */ + $prefs = SystemPreference::findByKey(...$keys); + + if (is_null($prefs) || count($prefs) === 0) { + $this->throwNotFound(); + } + + return $prefs; + } +} diff --git a/packages/backend-php/app/handlers/v2/UserServiceHandler2.php b/packages/backend-php/app/handlers/v2/UserServiceHandler2.php new file mode 100644 index 00000000..c7eff01c --- /dev/null +++ b/packages/backend-php/app/handlers/v2/UserServiceHandler2.php @@ -0,0 +1,772 @@ +AllowedColumns()); + } + + /** + * @permission administrator + * + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * @param int $membershipid + * + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException + * + * @return \app\domain\User + */ + public function Create($username, $password, $email, $fname, $lname, $membershipid): User { + if (User::exists($username, $email)) { + throw new UserAlreadyExistsException(); + } + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user = new User(); + + $user->username = $username; + $user->password = $hashedPassword; + $user->email = $email; + $user->payment_email = $email; + $user->stripe_email = $email; + $user->fname = $fname; + $user->lname = $lname; + $user->active = UserActiveEnum::PENDING->value; + $user->token = bin2hex(openssl_random_pseudo_bytes(8)); + + $user->save(); + + try { + $this->UpdateMembership($user->id, $membershipid); + } catch (\Exception $ex) { + // Ignore result + } + + $protocol = + (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 + ? StringLiterals::HTTPS_PREFIX + : StringLiterals::HTTP_PREFIX; + $domainName = $_SERVER['HTTP_HOST'] . '/'; + + EmailAdapter2::getInstance()->EmailUser($user, 'welcome', [ + 'token' => $user->token, + 'host' => $protocol . $domainName + ]); + + return $this->getUserById($user->id); + } + + /** + * @permission user|administrator + * + * @param int $userid + * + * @return bool + */ + public function GetStanding($userid): bool { + $user = $this->getUserById($userid); + + return new DateTime($user->mem_expire) > new DateTime(); + } + + /** + * @permission administrator + * + * @return array + * + * @phpstan-ignore missingType.iterableValue + */ + public function GetStatuses(): array { + return [ + ['title' => 'Active', 'code' => UserActiveEnum::ACTIVE->value], + ['title' => 'Pending', 'code' => UserActiveEnum::PENDING->value], + ['title' => 'Inactive', 'code' => UserActiveEnum::INACTIVE->value], + ['title' => 'Banned', 'code' => UserActiveEnum::BANNED->value] + ]; + } + + /** + * @permission administrator|user + * + * @param int $userid + * + * @return \app\domain\User|null + */ + public function GetUser($userid): User|null { + if (CurrentUser::getIdentity() == $userid || CurrentUser::hasAnyPermissions('administrator')) { + return $this->getUserById($userid); + } + + return null; + } + + /** + * Get the privileges that are grantable to the specified user by the user calling this service method. + * + * @permission grants + * + * @param int $userid + * + * @return array|array + */ + public function GetUserGrantablePrivileges($userid): array { + /** @var User $user */ + $user = $this->getUserById($userid); + + if (CurrentUser::canGrantAllPermissions('*')) { + return $user->getPrivilegeCodes(); + } + + $me = $this->getUserById(CurrentUser::getIdentity()); + + return array_intersect($user->getPrivilegeCodes(), $me->getGrantCodes()); + } + + /** + * @permission administrator + * + * @return \app\domain\User[] + */ + public function GetUsers(): array { + return User::findAll(); + } + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @throws \vhs\domain\exceptions\DomainException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function GrantPrivilege($userid, $privilege): bool { + if (!CurrentUser::canGrantAllPermissions($privilege)) { + throw new UnauthorizedException('Current user is not allowed to grant this privilege.'); + } + + /** @var User $user */ + $user = $this->getUserById($userid); + + if ($user === null) { + throw new DomainException('User not found'); + } + + /** @var Privilege $priv */ + $priv = Privilege::findByCode($privilege); + + if ($priv === null) { + throw new DomainException('Privilege not found'); + } + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $p) { + if ($p->code == $priv->code) { + throw new DomainException('Privilege already granted'); + } + } + + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->add($priv); + + return $user->save(); + } + + /** + * @permission administrator|grants + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\User[] + */ + public function ListUsers($page, $size, $columns, $order, $filters): array { + return User::page($page, $size, $columns, $order, $filters, $this->AllowedColumns()); + } + + /** + * @permission administrator + * + * @param int $userid + * @param string $privileges + * + * @return bool + */ + public function PutUserPrivileges($userid, $privileges): bool { + $user = $this->getUserById($userid); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->remove($priv); + } + + foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->add($priv); + } + + return $user->save(); + } + + /** + * @permission anonymous + * + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException + * + * @return \app\domain\User + */ + public function Register($username, $password, $email, $fname, $lname): User { + if (User::exists($username, $email)) { + throw new UserAlreadyExistsException(); + } + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user = new User(); + + $user->username = $username; + $user->password = $hashedPassword; + $user->email = $email; + $user->fname = $fname; + $user->lname = $lname; + $user->active = UserActiveEnum::PENDING->value; + $user->token = bin2hex(openssl_random_pseudo_bytes(8)); + + $user->save(); + + $protocol = + (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 + ? StringLiterals::HTTPS_PREFIX + : StringLiterals::HTTP_PREFIX; + $domainName = $_SERVER['HTTP_HOST'] . '/'; + + EmailAdapter2::getInstance()->EmailUser($user, 'welcome', [ + 'token' => $user->token, + 'host' => $protocol . $domainName + ]); + + return $user; + } + + /** + * @permission anonymous + * + * @param string $email + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function RequestPasswordReset($email): ServiceResponseSuccess|ServiceResponseError { + $user = User::findByEmail($email)[0]; + + if ($user === null) { + return new ServiceResponseErrorUserNotFoundByEmailAddress(); + } + + $request = new PasswordResetRequest(); + + $request->token = bin2hex(openssl_random_pseudo_bytes(8)); + $request->userid = $user->id; + + $request->save(); + + $protocol = + (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 + ? StringLiterals::HTTPS_PREFIX + : StringLiterals::HTTP_PREFIX; + $domainName = $_SERVER['HTTP_HOST'] . '/'; + + EmailAdapter2::getInstance()->EmailUser($user, 'recover', [ + 'token' => $request->token, + 'host' => $protocol . $domainName + ]); + + return new ServiceResponseSuccess(); + } + + /** + * @permission user + * + * @param string $email + * + * @return bool|string|null + */ + public function RequestSlackInvite($email): bool|string|null { + $ch = curl_init('http://slack-invite:3000/invite'); + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, 'email=' . $email); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_FORBID_REUSE, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); + + $error = null; + + if (!($response = curl_exec($ch))) { + $error = 'Error: Got ' . curl_error($ch) . " when request slack invite for email: '" . $email . "'"; + } + + curl_close($ch); + + if (!($error === null)) { + // $this->context->log($error); NOSONAR + return $error; + } + + return $response; + } + + /** + * @permission anonymous + * + * @param string $token + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException + * + * @return \app\dto\ServiceResponseError|\app\dto\ServiceResponseSuccess + */ + public function ResetPassword($token, $password): ServiceResponseSuccess|ServiceResponseError { + $requests = PasswordResetRequest::findByToken($token); + + if (!($requests === null) && count($requests) == 1) { + /** @var PasswordResetRequest $request */ + $request = $requests[0]; + $created = new DateTime($request->created); + $userid = $request->userid; + + $request->delete(); + $created->modify('+2 hours'); + if ($created > new DateTime()) { + $user = $this->getUserById($userid); + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user->password = $hashedPassword; + + $user->save(); + + return new ServiceResponseSuccess(); + } + } else { + $users = User::findByToken($token); + + if (!($users === null) && count($users) == 1) { + $user = $users[0]; + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user->token = null; + + $user->password = $hashedPassword; + $user->save(); + + return new ServiceResponseSuccess(); + } + } + + return new ServiceResponseErrorInvalidToken(); + } + + /** + * @permission grants + * + * @param int $userid + * @param string $privilege + * + * @throws \vhs\domain\exceptions\DomainException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return bool + */ + public function RevokePrivilege($userid, $privilege): bool { + if (!CurrentUser::canGrantAllPermissions($privilege)) { + throw new UnauthorizedException('Current user is not allowed to grant this privilege.'); + } + + /** @var User $user */ + $user = $this->getUserById($userid); + + if ($user === null) { + throw new DomainException('User not found'); + } + + /** @var Privilege $priv */ + $priv = Privilege::findByCode($privilege); + + if ($priv === null) { + throw new DomainException('Privilege not found'); + } + + $remove = null; + + // TODO fix typing + /** @disregard P1006 override */ + foreach ($user->privileges->all() as $p) { + if ($p->code == $priv->code) { + $remove = $p; + } + } + + if ($remove === null) { + throw new DomainException('Privilege instance not found'); + } + + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->remove($remove); + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param bool|string $cash + * + * @return bool + */ + public function UpdateCash($userid, $cash): bool { + $user = $this->getUserById($userid); + + $user->cash = boolval($cash); + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateEmail($userid, $email): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->email = $email; + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param string $date + * + * @return bool + */ + public function UpdateExpiry($userid, $date): bool { + $user = $this->getUserById($userid); + + $user->mem_expire = (new DateTime($date))->format('Y-m-d H:i:s'); + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param mixed $membershipid + * + * @throws \app\exceptions\InvalidInputException + * + * @return bool + */ + public function UpdateMembership($userid, $membershipid): bool { + $user = $this->getUserById($userid); + + /** @var Membership|null */ + $membership = Membership::find($membershipid); + + if ($membership === null) { + throw new InvalidInputException('Invalid user or membership type'); + } + + $user->membership = $membership; + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $fname + * @param string $lname + * + * @return bool + */ + public function UpdateName($userid, $fname, $lname): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->fname = $fname; + $user->lname = $lname; + + return $user->save(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param bool $subscribe + * + * @return bool + */ + public function UpdateNewsletter($userid, $subscribe): bool { + $user = $this->getUserById($userid); + + $user->newsletter = $subscribe ? true : false; + + return $user->save(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException + * + * @return bool + */ + public function UpdatePassword($userid, $password): bool { + $user = $this->getUserById($userid); + + $hashedPassword = PasswordUtil::hash($password); + + if ($hashedPassword === null) { + throw new InvalidPasswordHashException(); + } + + $user->password = $hashedPassword; + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdatePaymentEmail($userid, $email): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->payment_email = $email; + + return $user->save(); + } + + /** + * @permission administrator + * + * @param int $userid + * @param string $status + * + * @return bool + */ + public function UpdateStatus($userid, $status): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('administrator') !== true) { + $this->throwNotFound(); + } + + switch ($status) { + case 'active': + case UserActiveEnum::ACTIVE->value: + case 'true': + $status = UserActiveEnum::ACTIVE->value; + + break; + case 'pending': + case UserActiveEnum::PENDING->value: + $status = UserActiveEnum::PENDING->value; + + break; + case 'banned': + case UserActiveEnum::BANNED->value: + $status = UserActiveEnum::BANNED->value; + + break; + default: + $status = UserActiveEnum::INACTIVE->value; + + break; + } + + $user->active = $status; + + return $user->save(); + } + + /** + * @permission administrator|full-profile + * + * @param int $userid + * @param string $email + * + * @return bool + */ + public function UpdateStripeEmail($userid, $email): bool { + $user = $this->getUserById($userid); + + if (CurrentUser::hasAnyPermissions('full-profile', 'administrator') !== true) { + $this->throwNotFound(); + } + + $user->stripe_email = $email; + + return $user->save(); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param string $username + * + * @return bool + */ + public function UpdateUsername($userid, $username): bool { + $user = $this->getUserById($userid); + + $user->username = $username; + + return $user->save(); + } + + /** + * Summary of AllowedColumns. + * + * @return string[]|null + */ + protected function AllowedColumns(): array|null { + if (CurrentUser::hasAnyPermissions('grants') && !CurrentUser::hasAnyPermissions('administrator')) { + return ['id', 'username', 'fname', 'lname', 'email']; + } else { + return null; + } + } + + /** + * getUserById. + * + * @param mixed $id + * + * @return \app\domain\User + */ + private function getUserById($id): User { + $result = User::find($id); + + if ($result === null) { + $this->throwNotFound(); + } + + return $result; + } +} diff --git a/packages/backend-php/app/handlers/v2/WebHookServiceHandler2.php b/packages/backend-php/app/handlers/v2/WebHookServiceHandler2.php new file mode 100644 index 00000000..a947d547 --- /dev/null +++ b/packages/backend-php/app/handlers/v2/WebHookServiceHandler2.php @@ -0,0 +1,303 @@ +addUserIDToFilters($userid, $filters); + + return WebHook::count($filters); + } + + /** + * @permission user + * + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid + * + + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\WebHook + */ + public function CreateHook($name, $description, $enabled, $url, $translation, $headers, $method, $eventid): WebHook { + $event = (new EventServiceHandler2($this->context))->GetEvent($eventid); + + $codes = []; + foreach ($event->privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions(...$codes))) { + throw new UnauthorizedException('Insufficient privileges to subscribe to event'); + } + + $hook = new WebHook(); + + $hook->name = $name; + $hook->description = $description; + $hook->enabled = $enabled; + $hook->url = $url; + $hook->translation = $translation; + $hook->headers = $headers; + $hook->method = $method; + $hook->event = $event; + $hook->userid = CurrentUser::getIdentity(); + + $hook->save(); + + return $hook; + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return void + */ + public function DeleteHook($id): void { + $hook = $this->getWebHookById($id); + + $hook->delete(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param bool $enabled + * + * @return bool + */ + public function EnableHook($id, $enabled): bool { + $hook = $this->getWebHookById($id); + + $hook->enabled = $enabled; + + return $hook->save(); + } + + /** + * @permission webhook|administrator + * + * @return \app\domain\WebHook[] + */ + public function GetAllHooks(): array { + return WebHook::findAll(); + } + + /** + * @permission user|administrator + * + * @param int $id + * + * @return \app\domain\WebHook|null + */ + public function GetHook($id): WebHook|null { + return $this->getWebHookById($id); + } + + /** + * @permission webhook|administrator + * + * @param string $domain + * @param string $event + * + * @return \app\domain\WebHook[] + */ + public function GetHooks($domain, $event): array { + return WebHook::findByDomainEvent($domain, $event); + } + + /** + * @permission administrator|webhook + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\WebHook[] + */ + public function ListHooks($page, $size, $columns, $order, $filters): array { + return WebHook::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param \vhs\domain\Filter|null $filters + * + * @return \app\domain\WebHook[] + */ + public function ListUserHooks($userid, $page, $size, $columns, $order, $filters): array { + $filters = $this->addUserIDToFilters($userid, $filters); + + $cols = explode(',', $columns); + + array_push($cols, 'userid'); + + $columns = implode(',', array_unique($cols)); + + return WebHook::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string|string[] $privileges + * + * @return bool + */ + public function PutHookPrivileges($id, $privileges): bool { + $hook = $this->getWebHookById($id); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($hook->privileges->all() as $priv) { + $hook->privileges->remove($priv); + } + + foreach ($privs as $priv) { + if (CurrentUser::hasAnyPermissions('administrator') || CurrentUser::hasAnyPermissions($priv->code)) { + $hook->privileges->add($priv); + } + } + + return $hook->save(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid + * + * @return bool + */ + public function UpdateHook($id, $name, $description, $enabled, $url, $translation, $headers, $method, $eventid): bool { + $hook = $this->getWebHookById($id); + + $event = (new EventServiceHandler2($this->context))->GetEvent($eventid); + + $hook->name = $name; + $hook->description = $description; + $hook->enabled = $enabled; + $hook->url = $url; + $hook->translation = $translation; + $hook->headers = $headers; + $hook->method = $method; + $hook->event = $event; + + return $hook->save(); + } + + /** + * addUserIDToFilters. + * + * @param mixed $userid + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \vhs\domain\Filter + */ + private function addUserIDToFilters($userid, $filters): Filter { + $userService2 = new UserServiceHandler2(); + + $user = $userService2->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } + + /** + * getWebHookById. + * + * @param mixed $id + * + * @return \app\domain\WebHook + */ + private function getWebHookById($id): WebHook { + /** @var \app\domain\WebHook|null $pref */ + $pref = WebHook::find($id); + + if (is_null($pref)) { + $this->throwNotFound(); + } + + return $pref; + } +} diff --git a/packages/backend-php/app/include.php b/packages/backend-php/app/include.php new file mode 100644 index 00000000..71a2e7fd --- /dev/null +++ b/packages/backend-php/app/include.php @@ -0,0 +1,51 @@ +setLogger($sqlLog); + +\vhs\database\Database::setEngine($mySqlEngine); + +// @phpstan-ignore ternary.alwaysFalse +$rabbitLog = DEBUG ? new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/rabbit.log') : new \vhs\loggers\SilentLogger(); + +\vhs\messaging\MessageQueue::setLogger($rabbitLog); +\vhs\messaging\MessageQueue::setRethrow(true); + +$rabbitMQ = new \vhs\messaging\engines\RabbitMQ\RabbitMQEngine( + new \vhs\messaging\engines\RabbitMQ\RabbitMQConnectionInfo(RABBITMQ_HOST, (int) RABBITMQ_PORT, RABBITMQ_USER, RABBITMQ_PASSWORD, RABBITMQ_VHOST) +); + +$rabbitMQ->setLogger($rabbitLog); + +\vhs\messaging\MessageQueue::setEngine($rabbitMQ); + +\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', ROOT_NAMESPACE_PATH)); + +// @phpstan-ignore ternary.alwaysFalse +$serviceLog = DEBUG ? new \vhs\loggers\FileLogger(\vhs\BasePath::getBasePath(false) . '/logs/service.log') : new \vhs\loggers\SilentLogger(); + +\vhs\services\ServiceRegistry::register($serviceLog, 'native', 'app\\endpoints\\native', ROOT_NAMESPACE_PATH); +\vhs\services\ServiceRegistry::register($serviceLog, 'v2', 'app\\endpoints\\v2', ROOT_NAMESPACE_PATH); +\vhs\services\ServiceRegistry::register($serviceLog, 'web', 'app\\endpoints\\web', ROOT_NAMESPACE_PATH); diff --git a/app/modules/HttpPaymentGatewayHandler.php b/packages/backend-php/app/modules/HttpPaymentGatewayHandler.php similarity index 80% rename from app/modules/HttpPaymentGatewayHandler.php rename to packages/backend-php/app/modules/HttpPaymentGatewayHandler.php index a947d648..2ff615f1 100644 --- a/app/modules/HttpPaymentGatewayHandler.php +++ b/packages/backend-php/app/modules/HttpPaymentGatewayHandler.php @@ -13,6 +13,7 @@ use vhs\web\HttpRequestHandler; use vhs\web\HttpServer; +/** @typescript */ class HttpPaymentGatewayHandler extends HttpRequestHandler { /** @var IPaymentGateway */ private $gateway; @@ -21,7 +22,14 @@ public function __construct(IPaymentGateway $gateway) { $this->gateway = $gateway; } - public function handle(HttpServer $server) { + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function handle(HttpServer $server): void { $server->clear(); $server->code(200); diff --git a/app/modules/HttpPaymentGatewayHandlerModule.php b/packages/backend-php/app/modules/HttpPaymentGatewayHandlerModule.php similarity index 80% rename from app/modules/HttpPaymentGatewayHandlerModule.php rename to packages/backend-php/app/modules/HttpPaymentGatewayHandlerModule.php index c90f4679..c337efcb 100644 --- a/app/modules/HttpPaymentGatewayHandlerModule.php +++ b/packages/backend-php/app/modules/HttpPaymentGatewayHandlerModule.php @@ -12,6 +12,7 @@ use app\gateways\IPaymentGateway; use vhs\web\modules\HttpRequestHandlerModule; +/** @typescript */ class HttpPaymentGatewayHandlerModule extends HttpRequestHandlerModule { /** * @return HttpPaymentGatewayHandlerModule @@ -28,6 +29,14 @@ final public static function getInstance() { return $aoInstance[$class]; } + /** + * register. + * + * @param \app\gateways\IPaymentGateway $gateway + * @param string|null $url + * + * @return void + */ public static function register(IPaymentGateway $gateway, $url = null) { $path = '/services/gateways/' . $gateway->Name(); if (!is_null($url)) { @@ -42,6 +51,11 @@ public static function register(IPaymentGateway $gateway, $url = null) { self::getInstance()->register_internal('PUT', $path, $handler); } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/app/monitors/DomainEventMonitor.php b/packages/backend-php/app/monitors/DomainEventMonitor.php similarity index 91% rename from app/monitors/DomainEventMonitor.php rename to packages/backend-php/app/monitors/DomainEventMonitor.php index 5bc5a8ba..dff26880 100644 --- a/app/monitors/DomainEventMonitor.php +++ b/packages/backend-php/app/monitors/DomainEventMonitor.php @@ -15,12 +15,28 @@ use vhs\messaging\MessageQueue; use vhs\monitors\Monitor; +/** @typescript */ class DomainEventMonitor extends Monitor { + /** + * fireEvent. + * + * @param mixed $event + * @param mixed $data + * + * @return void + */ public function fireEvent($event, $data) { MessageQueue::publish($event->domain, $event->event, json_encode($data)); } - public function Init(Logger &$logger = null) { + /** + * Init. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public function Init(?Logger &$logger = null) { $events = Event::findAll(); foreach ($events as $event) { @@ -28,7 +44,11 @@ public function Init(Logger &$logger = null) { continue; } - /** @var Domain $domainClass */ + /** + * @var string $domainClass + * + * @phpstan-ignore varTag.nativeType + */ $domainClass = '\\app\\domain\\' . $event->domain; /** @@ -50,6 +70,7 @@ public function Init(Logger &$logger = null) { $domainClass::onAnyBeforeCreate(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; case 'before:deleted': @@ -57,6 +78,7 @@ public function Init(Logger &$logger = null) { $domainClass::onAnyBeforeDelete(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; case 'before:updated': @@ -64,6 +86,7 @@ public function Init(Logger &$logger = null) { $domainClass::onAnyBeforeUpdate(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; case 'before:saved': @@ -71,6 +94,7 @@ public function Init(Logger &$logger = null) { $domainClass::onAnyBeforeSave(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; case 'changed': @@ -88,6 +112,7 @@ public function Init(Logger &$logger = null) { $domainClass::onAnyCreated(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; case 'deleted': @@ -95,6 +120,7 @@ public function Init(Logger &$logger = null) { $domainClass::onAnyDeleted(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; case 'updated': @@ -102,6 +128,7 @@ public function Init(Logger &$logger = null) { $domainClass::onAnyUpdated(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; case 'saved': @@ -109,10 +136,12 @@ public function Init(Logger &$logger = null) { $domainClass::onAnySaved(function (...$args) use ($event) { $this->fireEvent($event, $args); }); + break; default: $logger->log(sprintf('Invalid domain event [%s] for domain [%s]', $event->event, $event->domain)); + break; } } diff --git a/app/monitors/PaymentMonitor.php b/packages/backend-php/app/monitors/PaymentMonitor.php similarity index 76% rename from app/monitors/PaymentMonitor.php rename to packages/backend-php/app/monitors/PaymentMonitor.php index 840ecb50..76649ebd 100644 --- a/app/monitors/PaymentMonitor.php +++ b/packages/backend-php/app/monitors/PaymentMonitor.php @@ -10,25 +10,39 @@ namespace app\monitors; use app\domain\Payment; -use app\domain\User; use app\processors\PaymentProcessor; -use Aws\CloudFront\Exception\Exception; use vhs\Logger; use vhs\monitors\Monitor; +/** @typescript */ class PaymentMonitor extends Monitor { /** @var Logger */ private $logger; + /** @var PaymentProcessor */ private $paymentProcessor; - public function Init(Logger &$logger = null) { + /** + * Init. + * + * @param \vhs\Logger|null $logger + * + * @return void + */ + public function Init(?Logger &$logger = null) { $this->logger = $logger; $this->paymentProcessor = new PaymentProcessor($logger); Payment::onAnyCreated([$this, 'paymentCreated']); } + /** + * paymentCreated. + * + * @param mixed $args + * + * @return void + */ public function paymentCreated($args) { try { $this->paymentProcessor->paymentCreated($args[0]->id); diff --git a/app/monitors/PaypalIpnMonitor.php b/packages/backend-php/app/monitors/PaypalIpnMonitor.php similarity index 90% rename from app/monitors/PaypalIpnMonitor.php rename to packages/backend-php/app/monitors/PaypalIpnMonitor.php index 45047987..499304c7 100644 --- a/app/monitors/PaypalIpnMonitor.php +++ b/packages/backend-php/app/monitors/PaypalIpnMonitor.php @@ -15,10 +15,18 @@ use vhs\Logger; use vhs\monitors\Monitor; +/** @typescript */ class PaypalIpnMonitor extends Monitor { /** @var Logger */ private $logger; + /** + * handleCreated. + * + * @param mixed $args + * + * @return void + */ public function handleCreated($args) { /** @var Ipn $ipn */ $ipn = $args[0]; @@ -83,7 +91,14 @@ public function handleCreated($args) { } } - public function Init(Logger &$logger = null) { + /** + * Init. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public function Init(?Logger &$logger = null) { $this->logger = &$logger; Ipn::onAnyCreated([$this, 'handleCreated']); } diff --git a/app/monitors/StripeEventMonitor.php b/packages/backend-php/app/monitors/StripeEventMonitor.php similarity index 91% rename from app/monitors/StripeEventMonitor.php rename to packages/backend-php/app/monitors/StripeEventMonitor.php index 27219db6..20b8961b 100644 --- a/app/monitors/StripeEventMonitor.php +++ b/packages/backend-php/app/monitors/StripeEventMonitor.php @@ -13,10 +13,18 @@ use vhs\Logger; use vhs\monitors\Monitor; +/** @typescript */ class StripeEventMonitor extends Monitor { /** @var Logger */ private $logger; + /** + * handleCreated. + * + * @param mixed $args + * + * @return void + */ public function handleCreated($args) { $this->logger->log(__METHOD__ . ': ' . json_encode($args, 1)); @@ -72,7 +80,7 @@ public function handleCreated($args) { $item_name = !is_null($line_item->plan->nickname) ? $line_item->plan->nickname : $item_name; } if (isset($line_item->price)) { - $item_amount = !is_null($line_item->price->unit_amount / 100) ? $line_item->price->unit_amount / 100 : $item_amount; + $item_amount = !is_null($line_item->price->unit_amount) ? $line_item->price->unit_amount / 100 : $item_amount; } if (isset($line_item->price)) { $item_number = !is_null($line_item->price->product) ? $line_item->price->product : $item_number; @@ -105,13 +113,24 @@ public function handleCreated($args) { } } - public function Init(Logger &$logger = null) { + /** + * Init. + * + * @param \vhs\Logger|null $logger + * + * @return void + */ + public function Init(?Logger &$logger = null) { $this->logger = &$logger; StripeEvent::onAnyCreated([$this, 'handleCreated']); } /** * Sourced from: https://stackoverflow.com/a/31330346. + * + * @param mixed $name + * + * @return array */ private function split_name($name) { $name = trim($name); diff --git a/app/processors/PaymentProcessor.php b/packages/backend-php/app/processors/PaymentProcessor.php similarity index 83% rename from app/processors/PaymentProcessor.php rename to packages/backend-php/app/processors/PaymentProcessor.php index 41b705f8..6e3abb4b 100644 --- a/app/processors/PaymentProcessor.php +++ b/packages/backend-php/app/processors/PaymentProcessor.php @@ -9,29 +9,45 @@ namespace app\processors; +use app\adapters\v2\EmailAdapter2; use app\constants\StringLiterals; use app\domain\Membership; use app\domain\Payment; use app\domain\User; +use app\dto\UserActiveEnum; use app\security\PasswordUtil; -use app\services\EmailService; use app\services\UserService; use DateTime; use vhs\Logger; use vhs\security\CurrentUser; use vhs\security\SystemPrincipal; +/** @typescript */ class PaymentProcessor { - private $emailService; + /** @var mixed */ private $host; - /** @var Logger */ + + /** @var \vhs\Logger|null */ private $logger; - public function __construct(Logger &$logger = null) { + /** + * __construct. + * + * @param \vhs\Logger|null $logger + * + * @return void + */ + public function __construct(?Logger &$logger = null) { $this->logger = $logger; - $this->emailService = new EmailService(); } + /** + * paymentCreated. + * + * @param mixed $id + * + * @return void + */ public function paymentCreated($id) { $suspended_user = CurrentUser::getPrincipal(); CurrentUser::setPrincipal(new SystemPrincipal()); @@ -51,7 +67,7 @@ public function paymentCreated($id) { return; } - /** @var User $user */ + /** @var User|null $user */ $user = null; $users = User::findByPaymentEmail($payment->payer_email); @@ -78,20 +94,34 @@ public function paymentCreated($id) { CurrentUser::setPrincipal($suspended_user); } + /** + * log. + * + * @param mixed $message + * + * @return void + */ private function log($message) { if (!is_null($this->logger)) { $this->logger->log("[PaymentMonitor] {$message}"); } } + /** + * processDonationPayment. + * + * @param \app\domain\User|null $user + * @param \app\domain\Payment $payment + * + * @return void + */ private function processDonationPayment(User $user = null, Payment $payment) { if (is_null($user)) { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ - 'subject' => '[Nomos] Unknown user made a random donation - ' . $payment->payer_fname . ' ' . $payment->lname, - 'message' => - $payment->fname . + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ + 'subject' => '[Nomos] Unknown user made a random donation - ' . $payment->payer_fname . ' ' . $payment->payer_lname, + 'message' => $payment->payer_fname . ' ' . - $payment->lname . + $payment->payer_lname . ' with email ' . $payment->payer_email . ' made a donation but we ' . @@ -106,7 +136,7 @@ private function processDonationPayment(User $user = null, Payment $payment) { $payment->currency ]); } else { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_donation_random', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_donation_random', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname, @@ -116,7 +146,7 @@ private function processDonationPayment(User $user = null, Payment $payment) { 'currency' => $payment->currency ]); - $this->emailService->EmailUser($user, 'donation_random', [ + EmailAdapter2::getInstance()->EmailUser($user, 'donation_random', [ 'fname' => $user->fname, 'lname' => $user->lname, 'rate_amount' => $payment->rate_amount, @@ -131,8 +161,16 @@ private function processDonationPayment(User $user = null, Payment $payment) { $payment->save(); } + /** + * processMemberPayment. + * + * @param \app\domain\User|null $user + * @param \app\domain\Payment $payment + * + * @return void + */ private function processMemberPayment(User $user = null, Payment $payment) { - /** @var Membership $membership */ + /** @var Membership|null $membership */ $membership = null; $memberships = Membership::findByCode($payment->item_number); @@ -171,7 +209,7 @@ private function processMemberPayment(User $user = null, Payment $payment) { return; } - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_newuser', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_newuser', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname, @@ -182,20 +220,19 @@ private function processMemberPayment(User $user = null, Payment $payment) { if ($user->membership_id != $membership->id) { $userService->UpdateMembership($user->id, $membership->id); } else { - if ($user->active == 'n') { + if ($user->active == UserActiveEnum::INACTIVE->value) { $userService->UpdateStatus($user->id, 'active'); } } $user = User::find($user->id); - if ($user->active != 'y') { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ - 'subject' => "[Nomos] User made payment but isn't active - " . $payment->payer_fname . ' ' . $payment->lname, - 'message' => - $payment->fname . + if ($user->active != UserActiveEnum::ACTIVE->value) { + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ + 'subject' => "[Nomos] User made payment but isn't active - " . $payment->payer_fname . ' ' . $payment->payer_lname, + 'message' => $payment->payer_fname . ' ' . - $payment->lname . + $payment->payer_lname . ' with email ' . $payment->payer_email . ' made a payment, but ' . @@ -234,7 +271,7 @@ private function processMemberPayment(User $user = null, Payment $payment) { $payment->status = 1; //processed $payment->save(); - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_payment', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_payment', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname, @@ -242,20 +279,27 @@ private function processMemberPayment(User $user = null, Payment $payment) { 'pp' => $payment->pp ]); - $this->emailService->EmailUser($user, 'payment', [ + EmailAdapter2::getInstance()->EmailUser($user, 'payment', [ 'host' => $this->host, 'fname' => $user->fname ]); } + /** + * processMembershipCardPayment. + * + * @param \app\domain\User|null $user + * @param \app\domain\Payment $payment + * + * @return void + */ private function processMembershipCardPayment(User $user = null, Payment $payment) { if (is_null($user)) { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_error', [ - 'subject' => '[Nomos] Unknown user purchased Membership Card - ' . $payment->payer_fname . ' ' . $payment->lname, - 'message' => - $payment->fname . + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_error', [ + 'subject' => '[Nomos] Unknown user purchased Membership Card - ' . $payment->payer_fname . ' ' . $payment->payer_lname, + 'message' => $payment->payer_fname . ' ' . - $payment->lname . + $payment->payer_lname . ' with email ' . $payment->payer_email . ' purchased a membership card but we ' . @@ -263,13 +307,13 @@ private function processMembershipCardPayment(User $user = null, Payment $paymen ' we can issue a member card.' ]); } else { - $this->emailService->Email(NOMOS_FROM_EMAIL, 'admin_membercard_purchased', [ + EmailAdapter2::getInstance()->Email(NOMOS_FROM_EMAIL, 'admin_membercard_purchased', [ 'email' => $payment->payer_email, 'fname' => $payment->payer_fname, 'lname' => $payment->payer_lname ]); - $this->emailService->EmailUser($user, 'membercard_purchased', [ + EmailAdapter2::getInstance()->EmailUser($user, 'membercard_purchased', [ 'fname' => $user->fname, 'lname' => $user->lname ]); @@ -282,6 +326,13 @@ private function processMembershipCardPayment(User $user = null, Payment $paymen $payment->save(); } + /** + * resolveLegacyPayments. + * + * @param \app\domain\Payment $payment + * + * @return \app\domain\Payment + */ private function resolveLegacyPayments(Payment $payment) { /* * select distinct item_name, item_number, rate_amount from payments where date > '2016-05-01'; diff --git a/app/resources/Resource.php b/packages/backend-php/app/resources/Resource.php similarity index 76% rename from app/resources/Resource.php rename to packages/backend-php/app/resources/Resource.php index e1369781..7a95f466 100644 --- a/app/resources/Resource.php +++ b/packages/backend-php/app/resources/Resource.php @@ -11,7 +11,9 @@ use vhs\Singleton; +/** @typescript */ class Resource extends Singleton { + /** @var array */ private $data; protected function __construct() { @@ -20,6 +22,13 @@ protected function __construct() { //$this->data[] = } + /** + * __get. + * + * @param string $name + * + * @return mixed + */ public function __get($name) { if (array_key_exists($name, $this->data)) { return $this->data[$name]; diff --git a/app/schema/AccessLogSchema.php b/packages/backend-php/app/schema/AccessLogSchema.php similarity index 84% rename from app/schema/AccessLogSchema.php rename to packages/backend-php/app/schema/AccessLogSchema.php index 9a11625d..dfae9244 100644 --- a/app/schema/AccessLogSchema.php +++ b/packages/backend-php/app/schema/AccessLogSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class AccessLogSchema extends Schema { public static function init() { $table = new Table('accesslog'); @@ -27,8 +28,12 @@ public static function init() { $table->addColumn('time', Type::DateTime(false, date('Y-m-d H:i:s'))); $table->addColumn('userid', Type::Int()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('accesslog', $table, $table->columns->userid)); return $table; diff --git a/packages/backend-php/app/schema/AccessTokenSchema.php b/packages/backend-php/app/schema/AccessTokenSchema.php new file mode 100644 index 00000000..b2638032 --- /dev/null +++ b/packages/backend-php/app/schema/AccessTokenSchema.php @@ -0,0 +1,63 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('token', Type::String()); + $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('userid', Type::Int()); + $table->addColumn('appclientid', Type::Int()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + + // TODO implement proper typing + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->appclientid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + AppClientSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + AppClientSchema::Columns()->id + ) + ); + + $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table)); + + return $table; + } +} diff --git a/packages/backend-php/app/schema/AppClientSchema.php b/packages/backend-php/app/schema/AppClientSchema.php new file mode 100644 index 00000000..e6d76657 --- /dev/null +++ b/packages/backend-php/app/schema/AppClientSchema.php @@ -0,0 +1,56 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('secret', Type::String()); + $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('userid', Type::Int()); + $table->addColumn('name', Type::String()); + $table->addColumn('description', Type::String()); + $table->addColumn('url', Type::String()); + $table->addColumn('redirecturi', Type::String()); + $table->addColumn('enabled', Type::Bool(false, false)); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('appclient', $table, $table->columns->userid)); + + return $table; + } +} diff --git a/app/schema/EmailSchema.php b/packages/backend-php/app/schema/EmailSchema.php similarity index 90% rename from app/schema/EmailSchema.php rename to packages/backend-php/app/schema/EmailSchema.php index f29e3957..fd6a40df 100644 --- a/app/schema/EmailSchema.php +++ b/packages/backend-php/app/schema/EmailSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class EmailSchema extends Schema { public static function init() { $table = new Table('email_templates'); @@ -27,6 +28,8 @@ public static function init() { $table->addColumn('body', Type::Text()); $table->addColumn('html', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('emailtemplate', $table)); diff --git a/packages/backend-php/app/schema/EventPrivilegeSchema.php b/packages/backend-php/app/schema/EventPrivilegeSchema.php new file mode 100644 index 00000000..bcbaec61 --- /dev/null +++ b/packages/backend-php/app/schema/EventPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('eventid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->eventid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->eventid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EventSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EventSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/EventSchema.php b/packages/backend-php/app/schema/EventSchema.php similarity index 90% rename from app/schema/EventSchema.php rename to packages/backend-php/app/schema/EventSchema.php index ecf066d2..81c27e06 100644 --- a/app/schema/EventSchema.php +++ b/packages/backend-php/app/schema/EventSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class EventSchema extends Schema { /** * @return Table @@ -29,6 +30,8 @@ public static function init() { $table->addColumn('description', Type::Text()); $table->addColumn('enabled', Type::Bool(false, false)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('event', $table)); diff --git a/app/schema/GenuineCardSchema.php b/packages/backend-php/app/schema/GenuineCardSchema.php similarity index 86% rename from app/schema/GenuineCardSchema.php rename to packages/backend-php/app/schema/GenuineCardSchema.php index 1bcc7f1d..c20839ab 100644 --- a/app/schema/GenuineCardSchema.php +++ b/packages/backend-php/app/schema/GenuineCardSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class GenuineCardSchema extends Schema { public static function init() { $table = new Table('genuinecard'); @@ -29,8 +30,12 @@ public static function init() { $table->addColumn('owneremail', Type::String(true, '', 255)); $table->addColumn('notes', Type::String(true, '', 255)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setAccess(PrivilegedAccess::GenerateAccess('genuinecard', $table, $table->columns->userid)); return $table; diff --git a/app/schema/IpnSchema.php b/packages/backend-php/app/schema/IpnSchema.php similarity index 92% rename from app/schema/IpnSchema.php rename to packages/backend-php/app/schema/IpnSchema.php index d5e646d2..a3a19c65 100644 --- a/app/schema/IpnSchema.php +++ b/packages/backend-php/app/schema/IpnSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class IpnSchema extends Schema { public static function init() { $table = new Table('ipnrequest'); @@ -30,6 +31,8 @@ public static function init() { $table->addColumn('item_number', Type::String(false, '', 255)); $table->addColumn('raw', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('ipn', $table)); diff --git a/packages/backend-php/app/schema/KeyPrivilegeSchema.php b/packages/backend-php/app/schema/KeyPrivilegeSchema.php new file mode 100644 index 00000000..e8290a2b --- /dev/null +++ b/packages/backend-php/app/schema/KeyPrivilegeSchema.php @@ -0,0 +1,62 @@ +addColumn('keyid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->keyid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + // TODO implement proper typing + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->keyid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + KeySchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + KeySchema::Columns()->id + ), + // TODO implement proper typing + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/packages/backend-php/app/schema/KeySchema.php b/packages/backend-php/app/schema/KeySchema.php new file mode 100644 index 00000000..3a1b549e --- /dev/null +++ b/packages/backend-php/app/schema/KeySchema.php @@ -0,0 +1,54 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('userid', Type::Int()); + $table->addColumn('type', Type::Enum('undefined', 'api', 'rfid', 'pin', 'github', 'google', 'slack')); + $table->addColumn('key', Type::String(true, null, 255)); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + $table->addColumn('expires', Type::DateTime()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('key', $table, $table->columns->userid)); + + return $table; + } +} diff --git a/packages/backend-php/app/schema/MembershipPrivilegeSchema.php b/packages/backend-php/app/schema/MembershipPrivilegeSchema.php new file mode 100644 index 00000000..fbda2153 --- /dev/null +++ b/packages/backend-php/app/schema/MembershipPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('membershipid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->membershipid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->membershipid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + MembershipSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + MembershipSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/MembershipSchema.php b/packages/backend-php/app/schema/MembershipSchema.php similarity index 92% rename from app/schema/MembershipSchema.php rename to packages/backend-php/app/schema/MembershipSchema.php index 96335691..0dafefc1 100644 --- a/app/schema/MembershipSchema.php +++ b/packages/backend-php/app/schema/MembershipSchema.php @@ -15,6 +15,9 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** + * @typescript + */ class MembershipSchema extends Schema { public static function init() { $table = new Table('memberships'); @@ -31,6 +34,8 @@ public static function init() { $table->addColumn('private', Type::Bool(false, false)); $table->addColumn('active', Type::Bool(false, false)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('membership', $table)); diff --git a/packages/backend-php/app/schema/PasswordResetRequestSchema.php b/packages/backend-php/app/schema/PasswordResetRequestSchema.php new file mode 100644 index 00000000..55ce319b --- /dev/null +++ b/packages/backend-php/app/schema/PasswordResetRequestSchema.php @@ -0,0 +1,51 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('userid', Type::Int()); + $table->addColumn('token', Type::String(true, null, 255)); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('passwordresetrequest', $table, $table->columns->userid)); + + return $table; + } +} diff --git a/packages/backend-php/app/schema/PaymentSchema.php b/packages/backend-php/app/schema/PaymentSchema.php new file mode 100644 index 00000000..4da50f26 --- /dev/null +++ b/packages/backend-php/app/schema/PaymentSchema.php @@ -0,0 +1,73 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('txn_id', Type::String(false, '', 100)); //txn_id + $table->addColumn('membership_id', Type::Int(true, 0)); + $table->addColumn('user_id', Type::Int(true, 0)); + $table->addColumn('payer_email', Type::String(true, null, 255)); + $table->addColumn('payer_fname', Type::String(true, null, 255)); + $table->addColumn('payer_lname', Type::String(true, null, 255)); + $table->addColumn('rate_amount', Type::String(false, '', 255)); + $table->addColumn('currency', Type::String(true, null, 4)); + $table->addColumn('date', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('pp', Type::Enum('PayPal', 'MoneyBookers', 'Stripe')); + $table->addColumn('ip', Type::String(true, null, 20)); + $table->addColumn('status', Type::Int(false, 0)); // 1==completed, anything else is "pending" + $table->addColumn('item_name', Type::String(true, null, 255)); + $table->addColumn('item_number', Type::String(true, null, 255)); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->membership_id, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + MembershipSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + MembershipSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->user_id, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('payment', $table, $table->columns->user_id)); + + return $table; + } +} diff --git a/app/schema/PrivilegeSchema.php b/packages/backend-php/app/schema/PrivilegeSchema.php similarity index 90% rename from app/schema/PrivilegeSchema.php rename to packages/backend-php/app/schema/PrivilegeSchema.php index 9424614a..9a563c43 100644 --- a/app/schema/PrivilegeSchema.php +++ b/packages/backend-php/app/schema/PrivilegeSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class PrivilegeSchema extends Schema { public static function init() { $table = new Table('privileges'); @@ -26,6 +27,8 @@ public static function init() { $table->addColumn('icon', Type::String(false, '', 255)); $table->addColumn('enabled', Type::Bool(false, false)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('privilege', $table)); diff --git a/packages/backend-php/app/schema/RefreshTokenSchema.php b/packages/backend-php/app/schema/RefreshTokenSchema.php new file mode 100644 index 00000000..46b2e184 --- /dev/null +++ b/packages/backend-php/app/schema/RefreshTokenSchema.php @@ -0,0 +1,63 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('token', Type::String()); + $table->addColumn('expires', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('userid', Type::Int()); + $table->addColumn('appclientid', Type::Int()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->appclientid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + AppClientSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + AppClientSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('accesstoken', $table, $table->columns->userid)); + + return $table; + } +} diff --git a/app/schema/SettingsSchema.php b/packages/backend-php/app/schema/SettingsSchema.php similarity index 99% rename from app/schema/SettingsSchema.php rename to packages/backend-php/app/schema/SettingsSchema.php index 094b453e..2adb545c 100644 --- a/app/schema/SettingsSchema.php +++ b/packages/backend-php/app/schema/SettingsSchema.php @@ -14,6 +14,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class SettingsSchema extends Schema { /** * @return Table diff --git a/app/schema/StripeEventSchema.php b/packages/backend-php/app/schema/StripeEventSchema.php similarity index 91% rename from app/schema/StripeEventSchema.php rename to packages/backend-php/app/schema/StripeEventSchema.php index 5f49d421..a95f48c3 100644 --- a/app/schema/StripeEventSchema.php +++ b/packages/backend-php/app/schema/StripeEventSchema.php @@ -13,6 +13,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class StripeEventSchema extends Schema { public static function init() { $table = new Table('stripe_events'); @@ -28,6 +29,8 @@ public static function init() { $table->addColumn('api_version', Type::String(false, '', 255)); $table->addColumn('raw', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('stripe_events', $table)); diff --git a/packages/backend-php/app/schema/SystemPreferencePrivilegeSchema.php b/packages/backend-php/app/schema/SystemPreferencePrivilegeSchema.php new file mode 100644 index 00000000..7c5dc492 --- /dev/null +++ b/packages/backend-php/app/schema/SystemPreferencePrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('systempreferenceid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->systempreferenceid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->systempreferenceid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + SystemPreferenceSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + SystemPreferenceSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/app/schema/SystemPreferenceSchema.php b/packages/backend-php/app/schema/SystemPreferenceSchema.php similarity index 89% rename from app/schema/SystemPreferenceSchema.php rename to packages/backend-php/app/schema/SystemPreferenceSchema.php index cf3cc66d..9e2b2ea5 100644 --- a/app/schema/SystemPreferenceSchema.php +++ b/packages/backend-php/app/schema/SystemPreferenceSchema.php @@ -15,6 +15,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class SystemPreferenceSchema extends Schema { public static function init() { $table = new Table('systempreferences'); @@ -25,6 +26,8 @@ public static function init() { $table->addColumn('enabled', Type::Bool(false, true)); $table->addColumn('notes', Type::Text()); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); $table->setAccess(PrivilegedAccess::GenerateAccess('systempreference', $table)); diff --git a/packages/backend-php/app/schema/UserPrivilegeSchema.php b/packages/backend-php/app/schema/UserPrivilegeSchema.php new file mode 100644 index 00000000..4caa9682 --- /dev/null +++ b/packages/backend-php/app/schema/UserPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('userid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->userid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/packages/backend-php/app/schema/UserSchema.php b/packages/backend-php/app/schema/UserSchema.php new file mode 100644 index 00000000..ee656d0c --- /dev/null +++ b/packages/backend-php/app/schema/UserSchema.php @@ -0,0 +1,75 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('username', Type::String(false, '', 255)); + $table->addColumn('password', Type::String(false, '', 255), false); + $table->addColumn('membership_id', Type::Int(false, 0)); + $table->addColumn('mem_expire', Type::DateTime(true, date('Y-m-d H:i:s'))); + $table->addColumn('trial_used', Type::Bool(false, false)); + $table->addColumn('email', Type::String(false, '', 255)); + $table->addColumn('fname', Type::String(false, '', 32)); + $table->addColumn('lname', Type::String(false, '', 32)); + $table->addColumn('token', Type::String(false, '0', 40)); + $table->addColumn('cookie_id', Type::String(false, '0', 64)); + $table->addColumn('newsletter', Type::Bool(false, false)); + $table->addColumn('cash', Type::Bool(false, false)); + $table->addColumn('userlevel', Type::Int(false, 1)); + $table->addColumn('notes', Type::Text()); + $table->addColumn('created', Type::DateTime(true, date('Y-m-d H:i:s'))); + $table->addColumn('lastlogin', Type::DateTime(true, date('Y-m-d H:i:s'))); + $table->addColumn('lastip', Type::String(true, '0', 16)); + $table->addColumn('avatar', Type::String(true, '0', 150)); + $table->addColumn( + 'active', + Type::Enum(UserActiveEnum::INACTIVE->value, UserActiveEnum::ACTIVE->value, UserActiveEnum::PENDING->value, UserActiveEnum::BANNED->value) + ); + $table->addColumn('paypal_id', Type::String(false, '', 255)); + $table->addColumn('payment_email', Type::String(false, '', 255)); + $table->addColumn('stripe_id', Type::String(false, '', 255)); + $table->addColumn('stripe_email', Type::String(false, '', 255)); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->membership_id, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + MembershipSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + MembershipSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('user', $table, $table->columns->id)); + + return $table; + } +} diff --git a/packages/backend-php/app/schema/WebHookPrivilegeSchema.php b/packages/backend-php/app/schema/WebHookPrivilegeSchema.php new file mode 100644 index 00000000..da6aea6c --- /dev/null +++ b/packages/backend-php/app/schema/WebHookPrivilegeSchema.php @@ -0,0 +1,60 @@ +addColumn('webhookid', Type::Int()); + $table->addColumn('privilegeid', Type::Int()); + $table->addColumn('created', Type::DateTime(false, date('Y-m-d H:i:s'))); + $table->addColumn('notes', Type::Text()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->webhookid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->privilegeid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->webhookid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + WebHookSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + WebHookSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->privilegeid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + PrivilegeSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + PrivilegeSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/packages/backend-php/app/schema/WebHookSchema.php b/packages/backend-php/app/schema/WebHookSchema.php new file mode 100644 index 00000000..c3622d88 --- /dev/null +++ b/packages/backend-php/app/schema/WebHookSchema.php @@ -0,0 +1,71 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('name', Type::String(false, '', 255)); + $table->addColumn('description', Type::Text()); + $table->addColumn('enabled', Type::Bool(false, false)); + $table->addColumn('userid', Type::Int()); + $table->addColumn('url', Type::String(false, '', 255)); + $table->addColumn('translation', Type::Text()); + $table->addColumn('headers', Type::Text()); + $table->addColumn('method', Type::String(false, 'POST', 32)); + $table->addColumn('eventid', Type::Int()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->userid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + UserSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + UserSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->eventid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EventSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EventSchema::Columns()->id + ) + ); + + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->setAccess(PrivilegedAccess::GenerateAccess('webhook', $table, $table->columns->userid)); + + return $table; + } +} diff --git a/packages/backend-php/app/security/Authenticate.php b/packages/backend-php/app/security/Authenticate.php new file mode 100644 index 00000000..925291e8 --- /dev/null +++ b/packages/backend-php/app/security/Authenticate.php @@ -0,0 +1,415 @@ +getUsername(), $credentials->getPassword()); + + break; + case 'app\\security\\credentials\\ApiCredentials': + /** @var ApiCredentials $credentials */ + self::keyLogin(Key::findByApiKey($credentials->getToken()), $credentials); + + break; + case 'app\\security\\credentials\\RfidCredentials': + /** @var RfidCredentials $credentials */ + self::keyLogin(Key::findByRfid($credentials->getToken()), $credentials); + + break; + case 'app\\security\\credentials\\PinCredentials': + /** @var PinCredentials $credentials */ + self::keyLogin(Key::findByPin($credentials->getToken()), $credentials); + + break; + case 'vhs\\security\\BearerTokenCredentials': + /** @var BearerTokenCredentials $credentials */ + self::bearerLogin($credentials); + + break; + default: + throw new InvalidCredentials('"Unsupported authentication type."'); + } + } + + /** + * logout. + * + * @return void + */ + public static function logout() { + CurrentUser::setPrincipal(new AnonPrincipal()); + } + + /** + * bearerLogin. + * + * @param BearerTokenCredentials $credentials + * + * @throws \app\exceptions\InvalidAccessTokenCredentialsException + * + * @return void + */ + private static function bearerLogin(BearerTokenCredentials $credentials) { + $ipaddr = self::getRemoteIP(); + + $token = null; + + try { + $token = AccessToken::findByToken($credentials->getToken()); + } catch (\Exception $ex) { + // no further action required + } + + if (is_null($token) || is_null($token->user)) { + AccessLog::log($credentials->getToken(), 'bearer', false, $ipaddr); + + throw new InvalidAccessTokenCredentialsException(); + } + + if ( + self::isUserValid($token->user) && + (is_null($token->client) || + ($token->client->enabled && (is_null($token->client->expires) || new DateTime($token->client->expires) > new DateTime()))) + ) { + CurrentUser::setPrincipal(self::buildPrincipal($token->user)); + + self::recordLogin($token->user, $ipaddr); + + AccessLog::log($credentials->getToken(), 'bearer', true, $ipaddr, $token->user->id); + } else { + AccessLog::log($credentials->getToken(), 'bearer', false, $ipaddr, $token->user->id); + + throw new InvalidAccessTokenCredentialsException(); + } + } + + /** + * @param \app\domain\User $user + * + * @return UserPrincipal + */ + private static function buildPrincipal($user) { + $membershipPrivs = []; + $privileges = []; + $grants = []; + + if ($user->valid) { + if (!is_null($user->membership)) { + $membershipPrivs = array_map(function ($privilege) { + return $privilege->code; + }, $user->membership->privileges->all()); + } + + $privileges = array_merge( + $membershipPrivs, + array_map( + function ($privilege) { + return $privilege->code; + }, + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->all() + ) + ); + + foreach ($privileges as $priv) { + if (strpos($priv, 'grant:') === 0) { + array_push($grants, substr($priv, 6)); + } + } + + if (count($grants) > 0) { + array_push($privileges, 'grants'); + } + } + + array_push($privileges, 'user'); + + return new UserPrincipal($user->id, $privileges, $grants, $user->username); + } + + /** + * @param string $username + * + * @throws \vhs\security\exceptions\InvalidCredentials + * + * @return User + */ + private static function findUser($username) { + $users = User::findByUsername($username); + + if (count($users) != 1) { + //Try e-mail Address + $users = User::findByEmail($username); + } + + if (count($users) != 1) { + throw new InvalidCredentials('"Incorrect username or password"'); + } + + return $users[0]; + } + + /** + * getRemoteIP. + * + * @return string|null + */ + private static function getRemoteIP() { + $ipaddr = null; + + if (array_key_exists('REMOTE_ADDR', $_SERVER)) { + $ipaddr = $_SERVER['REMOTE_ADDR']; + } + + return $ipaddr; + } + + /** + * isUserValid. + * + * @param \app\domain\User $user + * + * @throws \vhs\security\exceptions\InvalidCredentials + * + * @return bool + */ + private static function isUserValid(User $user) { + switch ($user->active) { + case UserActiveEnum::INACTIVE->value: //not active + throw new InvalidCredentials('"Your account is not activated"'); + case UserActiveEnum::ACTIVE->value: //yes they are active + return true; + case UserActiveEnum::PENDING->value: //pending email verification + throw new InvalidCredentials('"You need to verify your email address"'); + case UserActiveEnum::BANNED->value: //banned + throw new InvalidCredentials('"Your account has been banned"'); + default: + return false; + } + } + + /** + * keyLogin. + * + * @param mixed $keys + * @param \app\security\credentials\TokenCredentials $credentials + * + * @throws \app\exceptions\InvalidKeyCredentialsException + * + * @return void + */ + private static function keyLogin($keys, TokenCredentials $credentials) { + $ipaddr = self::getRemoteIP(); + + if (count($keys) != 1) { + AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr); + + throw new InvalidKeyCredentialsException(); + } + + $key = $keys[0]; + $identity = null; + $name = 'token:' . $key->id . ':'; + + $privileges = array_map(function ($priviledge) { + return $priviledge->code; + }, $key->privileges->all()); + + if (!is_null($key->userid) && $key->userid != '0') { + try { + $user = User::find($key->userid); + } catch (\Exception $ex) { + AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr, $key->userid); + + throw new InvalidKeyCredentialsException(); + } + + if (!is_null($user) && self::isUserValid($user)) { + $identity = $user->id; + $name .= $user->username; + + if (in_array('inherit', $privileges)) { + array_push($privileges, 'user'); + $privileges = array_merge( + $privileges, + array_map(function ($privilege) { + return $privilege->code; + }, $user->membership->privileges->all()), + array_map( + function ($privilege) { + return $privilege->code; + }, + // TODO fix typing + /** @disregard P1006 override */ + $user->privileges->all() + ) + ); + } + } else { + AccessLog::log($credentials->getToken(), $credentials->getType(), false, $ipaddr); + + throw new InvalidKeyCredentialsException(); + } + } + + $grants = []; + + foreach ($privileges as $priv) { + if (strpos($priv, 'grant:') === 0) { + array_push($grants, substr($priv, 6)); + } + } + + if (count($grants) > 0) { + array_push($privileges, 'grants'); + } + + CurrentUser::setPrincipal(new TokenPrincipal(id: $identity, permissions: $privileges, grants: $grants, name: $name)); + + AccessLog::log($credentials->getToken(), $credentials->getType(), true, $ipaddr, $key->userid); + } + + /** + * record login. + * + * @param \app\domain\User $user + * @param string $ipaddr + * + * @return void + */ + private static function recordLogin($user, $ipaddr) { + $user->lastlogin = date(Database::DateFormat()); + $user->lastip = $ipaddr; + + try { + $user->save(); + } catch (\Exception $ex) { + self::logout(); + + throw $ex; + } + } + + /** + * userLogin. + * + * @param string $username + * @param string $password + * @param bool $authonly + * + + * @throws \vhs\security\exceptions\InvalidCredentials + * + * @return \app\domain\User + */ + private static function userLogin($username, $password, $authonly = false) { + $ipaddr = self::getRemoteIP(); + + try { + $user = self::findUser($username); + } catch (\Exception $ex) { + AccessLog::log($username, 'userpass', false, $ipaddr); + + throw $ex; + } + + if (self::isUserValid($user) && PasswordUtil::check($password, $user->password)) { + if (!$authonly) { + CurrentUser::setPrincipal(self::buildPrincipal($user)); + } + + self::recordLogin($user, $ipaddr); + + AccessLog::log($username, 'userpass', true, $ipaddr, $user->id); + + return $user; + } else { + AccessLog::log($username, 'userpass', false, $ipaddr, $user->id); + + throw new InvalidCredentials('"Incorrect username or password"'); + } + } +} diff --git a/packages/backend-php/app/security/ColumnPrivilegedAccess.php b/packages/backend-php/app/security/ColumnPrivilegedAccess.php new file mode 100644 index 00000000..2bb710e0 --- /dev/null +++ b/packages/backend-php/app/security/ColumnPrivilegedAccess.php @@ -0,0 +1,119 @@ +column = $column; + $this->privileges = $privileges; + } + + /** + * CanRead. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ + public function CanRead($record, Table $table, Column $column) { + return $column === $this->column && $this->hasPrivilegedAccess($record, ...$this->privileges); + } + + /** + * CanWrite. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ + public function CanWrite($record, Table $table, Column $column) { + return $column === $this->column && $this->hasPrivilegedAccess($record, ...$this->privileges); + } + + /** + * jsonSerialize. + * + * @return mixed + */ + public function jsonSerialize(): mixed { + return [ + 'type' => 'column', + 'column' => [ + 'table' => $this->column->table->name, + 'name' => $this->column->name, + 'type' => $this->column->type + ], + 'privileges' => $this->privileges, + 'checks' => $this->checks + ]; + } + + /** + * serialize. + * + * @return string + */ + public function serialize(): string { + return json_encode($this->__serialize()); + } + + /** + * __serialize. + * + * @return array + */ + public function __serialize(): array { + return [ + 'type' => 'column', + 'column' => [ + 'table' => $this->column->table->name, + 'name' => $this->column->name, + 'type' => $this->column->type + ], + 'privileges' => $this->privileges, + 'checks' => $this->checks + ]; + } + + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ + public function __unserialize($data): void { + // TODO maybe implement? + } +} diff --git a/packages/backend-php/app/security/HttpApiAuthModule.php b/packages/backend-php/app/security/HttpApiAuthModule.php new file mode 100644 index 00000000..7c7d259a --- /dev/null +++ b/packages/backend-php/app/security/HttpApiAuthModule.php @@ -0,0 +1,80 @@ +authorizer = $authorizer; + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + if (array_key_exists('X-Api-Key', $server->request->headers) && $this->authorizer->isAuthenticated()) { + $this->authorizer->logout(); + } + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return void + */ + public function handle(HttpServer $server) { + if (array_key_exists('X-Api-Key', $server->request->headers) && !$this->authorizer->isAuthenticated()) { + try { + $this->authorizer->login(new ApiCredentials($server->request->headers['X-Api-Key'])); + } catch (\Exception $ex) { + throw new UnauthorizedException($ex->getMessage()); + } + } + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + if (get_class($ex) === 'vhs\\security\\exceptions\\UnauthorizedException') { + $server->clear(); + $server->header('HTTP/1.0 401 Unauthorized'); + $server->code(401); + $server->end(); + } + } +} diff --git a/packages/backend-php/app/security/PasswordUtil.php b/packages/backend-php/app/security/PasswordUtil.php new file mode 100644 index 00000000..b1ac8819 --- /dev/null +++ b/packages/backend-php/app/security/PasswordUtil.php @@ -0,0 +1,78 @@ +ownerColumn = $ownerColumn; + $this->checks = []; + } + + /** + * GenerateAccess. + * + * @param mixed $key + * @param \vhs\database\Table $table + * @param \vhs\database\Column $ownerColumn + * + * @return \vhs\database\access\IAccess + */ + public static function GenerateAccess($key, Table $table, ?Column $ownerColumn = null) { + $access = null; + $child = null; + + if (is_null($ownerColumn)) { + $access = new TablePrivilegedAccess(null, $table, 'access:' . $key); + $child = $access; + } else { + $access = new PrivilegedAccess($ownerColumn); + $child = $access->Table($table, 'access:' . $key); + } + foreach ($table->columns->all() as $column) { + $child->Column($column, 'access:' . $key, 'access:' . $key . ':' . $column->name); + } + + return $access; + } + + /** + * CanRead. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ + public function CanRead($record, Table $table, Column $column) { + $access = false; + foreach ($this->checks as $check) { + $access &= $check->CanRead($record, $table, $column); + } + + return $access; + } + + /** + * CanWrite. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ + public function CanWrite($record, Table $table, Column $column) { + $access = false; + foreach ($this->checks as $check) { + $access &= $check->CanWrite($record, $table, $column); + } + + return $access; + } + + /** + * Column. + * + * @param \vhs\database\Column $column + * @param string ...$privileges + * + * @return \vhs\database\access\IAccess + */ + public function Column(Column $column, ...$privileges) { + $access = new ColumnPrivilegedAccess($this->ownerColumn, $column, ...$privileges); + + $this->Register($access); + + return $access; + } + + /** + * hasPrivilegedAccess. + * + * @param mixed $record + * @param string ...$privileges + * + * @return bool + */ + public function hasPrivilegedAccess($record, ...$privileges) { + if (CurrentUser::hasAnyPermissions('administrator')) { + return true; + } + + if (in_array('owner', $privileges) && $this->IsOwner($record)) { + return true; + } + + return CurrentUser::hasAnyPermissions(...$privileges); + } + + /** + * IsOwner. + * + * @param mixed $record + * + * @return bool + */ + public function IsOwner($record) { + return array_key_exists($this->ownerColumn->name, $record) && $record[$this->ownerColumn->name] === CurrentUser::getIdentity(); + } + + /** + * Specify data which should be serialized to JSON. + * + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource + * + * @since 5.4.0 + */ + public function jsonSerialize(): mixed { + return [ + 'type' => 'ownership', + 'ownership' => [ + 'table' => $this->ownerColumn->table->name, + 'name' => $this->ownerColumn->name, + 'type' => $this->ownerColumn->type + ], + 'checks' => $this->checks + ]; + } + + /** + * Register. + * + * @param \vhs\database\access\IAccess ...$checks + * + * @return void + */ + public function Register(IAccess ...$checks) { + foreach ($checks as $check) { + array_push($this->checks, $check); + } + } + + /** + * String representation of object. + * + * @link http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or null + * + * @since 5.1.0 + */ + public function serialize(): string { + return json_encode($this->__serialize()); + } + + /** + * Table. + * + * @param \vhs\database\Table $table + * @param string ...$privileges + * + * @return \app\security\TablePrivilegedAccess + */ + public function Table(Table $table, ...$privileges) { + $access = new TablePrivilegedAccess($this->ownerColumn, $table, ...$privileges); + + $this->Register($access); + + return $access; + } + + /** + * Constructs the object. + * + * @link http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized

+ * The string representation of the object. + *

+ * + * @return void + * + * @since 5.1.0 + */ + public function unserialize($serialized) { + // TODO: Implement unserialize() method. + } + + /** + * __serialize. + * + * @return array + */ + public function __serialize(): array { + return [ + 'type' => 'ownership', + 'ownership' => [ + 'table' => $this->ownerColumn->table->name, + 'name' => $this->ownerColumn->name, + 'type' => $this->ownerColumn->type + ], + 'checks' => $this->checks + ]; + } + + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ + public function __unserialize($data): void { + // TODO: Implement __unserialize() method. + } +} diff --git a/packages/backend-php/app/security/TablePrivilegedAccess.php b/packages/backend-php/app/security/TablePrivilegedAccess.php new file mode 100644 index 00000000..2bbf7e3b --- /dev/null +++ b/packages/backend-php/app/security/TablePrivilegedAccess.php @@ -0,0 +1,104 @@ +table = $table; + $this->privileges = $privileges; + } + + /** + * CanRead. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ + public function CanRead($record, Table $table, Column $column) { + return $table === $this->table && $this->hasPrivilegedAccess($record, ...$this->privileges); + } + + /** + * CanWrite. + * + * @param mixed $record + * @param \vhs\database\Table $table + * @param \vhs\database\Column $column + * + * @return bool + */ + public function CanWrite($record, Table $table, Column $column) { + return $table === $this->table && $this->hasPrivilegedAccess($record, ...$this->privileges); + } + + /** + * jsonSerialize. + * + * @return mixed + */ + public function jsonSerialize(): mixed { + return [ + 'type' => 'table', + 'table' => $this->table->name, + 'privileges' => $this->privileges, + 'checks' => $this->checks + ]; + } + + /** + * serialize. + * + * @return string + */ + public function serialize(): string { + return json_encode($this->__serialize()); + } + + /** + * __serialize. + * + * @return array + */ + public function __serialize(): array { + return [ + 'type' => 'table', + 'table' => $this->table->name, + 'privileges' => $this->privileges, + 'checks' => $this->checks + ]; + } + + public function __unserialize($data): void { + // TODO maybe implement? + } +} diff --git a/packages/backend-php/app/security/TokenPrincipal.php b/packages/backend-php/app/security/TokenPrincipal.php new file mode 100644 index 00000000..c08eeccc --- /dev/null +++ b/packages/backend-php/app/security/TokenPrincipal.php @@ -0,0 +1,128 @@ +id = $id; + $this->permissions = $permissions; + $this->grants = $grants; + $this->name = $name; + } + + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAllPermissions(...$permission) { + return in_array('*', $this->grants) || count(array_diff($permission, $this->grants)) == 0; + } + + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAnyPermissions(...$permission) { + return in_array('*', $this->grants) || count(array_intersect($permission, $this->grants)) > 0; + } + + /** + * getIdentity. + * + * @return mixed + */ + public function getIdentity() { + return $this->id; + } + + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAllPermissions(...$permission) { + return count(array_diff($permission, $this->permissions)) == 0; + } + + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAnyPermissions(...$permission) { + return count(array_intersect($permission, $this->permissions)) > 0; + } + + /** + * isAnon. + * + * @return bool + */ + public function isAnon() { + return false; + } + + /** + * __toString. + * + * @return string + */ + public function __toString() { + return $this->name; + } +} diff --git a/packages/backend-php/app/security/UserPrincipal.php b/packages/backend-php/app/security/UserPrincipal.php new file mode 100644 index 00000000..122e31f1 --- /dev/null +++ b/packages/backend-php/app/security/UserPrincipal.php @@ -0,0 +1,144 @@ +id = $id; + $this->permissions = $permissions; + $this->grants = $grants; + $this->username = $username; + } + + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAllPermissions(...$permission) { + return in_array('*', $this->grants) || count(array_diff($permission, $this->grants)) == 0; + } + + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAnyPermissions(...$permission) { + return in_array('*', $this->grants) || count(array_intersect($permission, $this->grants)) > 0; + } + + /** + * getIdentity. + * + * @return mixed + */ + public function getIdentity() { + return $this->id; + } + + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAllPermissions(...$permission) { + return count(array_diff($permission, $this->permissions)) == 0; + } + + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAnyPermissions(...$permission) { + return count(array_intersect($permission, $this->permissions)) > 0; + } + + /** + * isAnon. + * + * @return bool + */ + public function isAnon() { + return false; + } + + /** + * jsonSerialize. + * + * @return mixed + */ + public function jsonSerialize(): mixed { + $data = []; + $data['id'] = $this->id; + $data['permissions'] = $this->permissions; + + return $data; + } + + /** + * __toString. + * + * @return string + */ + public function __toString() { + return 'user:' . $this->username; + } +} diff --git a/app/security/credentials/ApiCredentials.php b/packages/backend-php/app/security/credentials/ApiCredentials.php similarity index 92% rename from app/security/credentials/ApiCredentials.php rename to packages/backend-php/app/security/credentials/ApiCredentials.php index 9a857a16..35de9b5b 100644 --- a/app/security/credentials/ApiCredentials.php +++ b/packages/backend-php/app/security/credentials/ApiCredentials.php @@ -9,6 +9,7 @@ namespace app\security\credentials; +/** @typescript */ class ApiCredentials extends TokenCredentials { public function getType() { return 'api'; diff --git a/app/security/credentials/PinCredentials.php b/packages/backend-php/app/security/credentials/PinCredentials.php similarity index 92% rename from app/security/credentials/PinCredentials.php rename to packages/backend-php/app/security/credentials/PinCredentials.php index ff5ea829..793cc7da 100644 --- a/app/security/credentials/PinCredentials.php +++ b/packages/backend-php/app/security/credentials/PinCredentials.php @@ -9,6 +9,7 @@ namespace app\security\credentials; +/** @typescript */ class PinCredentials extends TokenCredentials { public function getType() { return 'pin'; diff --git a/app/security/credentials/RfidCredentials.php b/packages/backend-php/app/security/credentials/RfidCredentials.php similarity index 92% rename from app/security/credentials/RfidCredentials.php rename to packages/backend-php/app/security/credentials/RfidCredentials.php index 66797fbb..138885ed 100644 --- a/app/security/credentials/RfidCredentials.php +++ b/packages/backend-php/app/security/credentials/RfidCredentials.php @@ -9,6 +9,7 @@ namespace app\security\credentials; +/** @typescript */ class RfidCredentials extends TokenCredentials { public function getType() { return 'rfid'; diff --git a/packages/backend-php/app/security/credentials/TokenCredentials.php b/packages/backend-php/app/security/credentials/TokenCredentials.php new file mode 100644 index 00000000..63eb91e5 --- /dev/null +++ b/packages/backend-php/app/security/credentials/TokenCredentials.php @@ -0,0 +1,44 @@ +token = $token; + } + + /** + * getType. + * + * @return string + */ + abstract public function getType(); + + /** + * getToken. + * + * @return string + */ + public function getToken() { + return $this->token; + } +} diff --git a/packages/backend-php/app/security/oauth/OAuthHelper.php b/packages/backend-php/app/security/oauth/OAuthHelper.php new file mode 100644 index 00000000..31608ec6 --- /dev/null +++ b/packages/backend-php/app/security/oauth/OAuthHelper.php @@ -0,0 +1,144 @@ +provider = $provider; + $this->userDetails = null; + $this->server = $server; + } + + /** + * redirectHost. + * + * @return string + */ + public static function redirectHost() { + $protocol = + defined('NOMOS_FORCE_HTTPS') || ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443) + ? StringLiterals::HTTPS_PREFIX + : StringLiterals::HTTP_PREFIX; + $domainName = $_SERVER['HTTP_HOST']; + + return $protocol . $domainName; + } + + /** + * linkAccount. + * + * @param mixed $serviceUID + * @param mixed $serviceType + * @param mixed $notes + * + * @return void + */ + public function linkAccount($serviceUID, $serviceType, $notes) { + if (!Authenticate::isAuthenticated()) { + print 'Not logged in'; + + exit(); + } + + //Update old keys even if they are assigned to other users + $keys = Key::findKeyAndType($serviceUID, $serviceType); + + if (!empty($keys)) { + $key = $keys[0]; + } else { + $key = new Key(); + } + + $key->key = $serviceUID; + $key->userid = CurrentUser::getIdentity(); + $key->type = $serviceType; + $key->notes = $notes; + + // TODO fix typing + /** @disregard P1006 override */ + $key->privileges->clear(); + + $key->save(); + + // TODO fix typing + /** @disregard P1006 override */ + $key->privileges->add(Privilege::findByCode('inherit')); + + $key->save(); + } + + /** + * processToken. + * + * @return \League\OAuth2\Client\Provider\ResourceOwnerInterface|null + */ + public function processToken() { + /** @var \League\OAuth2\Client\Token\AccessToken */ + $token = $this->provider->getAccessToken('authorization_code', [ + 'code' => $_GET['code'] + ]); + + if (!is_null($token)) { + $this->userDetails = $this->provider->getResourceOwner($token); + + return $this->userDetails; + } + + return null; + } + + /** + * requestAuth. + * + * @param array $options + * + * @return void + * + * @phpstan-ignore missingType.iterableValue + */ + public function requestAuth(array $options = []) { + // If we don't have an authorization code then get one + $authUrl = $this->provider->getAuthorizationUrl($options); + + $this->server->clear(); + $this->server->redirect($authUrl); + $this->server->end(); + } +} diff --git a/app/security/oauth/modules/GithubOAuthHandler.php b/packages/backend-php/app/security/oauth/modules/GithubOAuthHandler.php similarity index 79% rename from app/security/oauth/modules/GithubOAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/GithubOAuthHandler.php index 1d0e2cf1..685b40f4 100644 --- a/app/security/oauth/modules/GithubOAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/GithubOAuthHandler.php @@ -13,11 +13,24 @@ use League\OAuth2\Client\Provider\Github; use vhs\web\HttpServer; +/** @typescript */ class GithubOAuthHandler extends OAuthHandler { + /** + * getUrl. + * + * @return string + */ public function getUrl() { return '/oauth/github.php'; } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $host = OauthHelper::redirectHost(); @@ -37,11 +50,11 @@ public function handle(HttpServer $server) { if (!isset($_GET['code'])) { $oauthHelper->requestAuth(); } else { - /** @var GithubResourceOwner | null */ + /** @var \League\OAuth2\Client\Provider\GithubResourceOwner */ $userDetails = $oauthHelper->processToken(); } - if ($_GET['action'] == 'link' && !is_null($userDetails)) { + if ($_GET['action'] == 'link' && $userDetails !== null) { $oauthHelper->linkAccount($userDetails->getId(), 'github', 'GitHub Account for ' . $userDetails->getNickname()); $server->clear(); diff --git a/app/security/oauth/modules/GoogleOAuthHandler.php b/packages/backend-php/app/security/oauth/modules/GoogleOAuthHandler.php similarity index 83% rename from app/security/oauth/modules/GoogleOAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/GoogleOAuthHandler.php index 3bd6ab0d..acb8c6fc 100644 --- a/app/security/oauth/modules/GoogleOAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/GoogleOAuthHandler.php @@ -13,11 +13,24 @@ use League\OAuth2\Client\Provider\Google; use vhs\web\HttpServer; +/** @typescript */ class GoogleOAuthHandler extends OAuthHandler { + /** + * getUrl. + * + * @return string + */ public function getUrl() { return '/oauth/google.php'; } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $host = OauthHelper::redirectHost(); @@ -37,7 +50,7 @@ public function handle(HttpServer $server) { if (!isset($_GET['code'])) { $oauthHelper->requestAuth(); } else { - /** @var GoogleUser | null */ + /** @var \League\OAuth2\Client\Provider\GoogleUser|null */ $userDetails = $oauthHelper->processToken(); } diff --git a/app/security/oauth/modules/OAuthHandler.php b/packages/backend-php/app/security/oauth/modules/OAuthHandler.php similarity index 77% rename from app/security/oauth/modules/OAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/OAuthHandler.php index 3521f165..ba6ffb0c 100644 --- a/app/security/oauth/modules/OAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/OAuthHandler.php @@ -11,6 +11,12 @@ use vhs\web\HttpRequestHandler; +/** @typescript */ abstract class OAuthHandler extends HttpRequestHandler { + /** + * getUrl. + * + * @return string + */ abstract public function getUrl(); } diff --git a/app/security/oauth/modules/OAuthHandlerModule.php b/packages/backend-php/app/security/oauth/modules/OAuthHandlerModule.php similarity index 79% rename from app/security/oauth/modules/OAuthHandlerModule.php rename to packages/backend-php/app/security/oauth/modules/OAuthHandlerModule.php index a86c3204..20f34c5c 100644 --- a/app/security/oauth/modules/OAuthHandlerModule.php +++ b/packages/backend-php/app/security/oauth/modules/OAuthHandlerModule.php @@ -11,6 +11,7 @@ use vhs\web\modules\HttpRequestHandlerModule; +/** @typescript */ class OAuthHandlerModule extends HttpRequestHandlerModule { /** * @return OAuthHandlerModule @@ -27,6 +28,13 @@ final public static function getInstance() { return $aoInstance[$class]; } + /** + * register. + * + * @param \app\security\oauth\modules\OAuthHandler $handler + * + * @return void + */ public static function register(OAuthHandler $handler) { $url = $handler->getUrl(); @@ -36,6 +44,11 @@ public static function register(OAuthHandler $handler) { self::getInstance()->register_internal('PUT', $url, $handler); } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/app/security/oauth/modules/SlackOAuthHandler.php b/packages/backend-php/app/security/oauth/modules/SlackOAuthHandler.php similarity index 85% rename from app/security/oauth/modules/SlackOAuthHandler.php rename to packages/backend-php/app/security/oauth/modules/SlackOAuthHandler.php index 39223820..bc4cee40 100644 --- a/app/security/oauth/modules/SlackOAuthHandler.php +++ b/packages/backend-php/app/security/oauth/modules/SlackOAuthHandler.php @@ -13,11 +13,24 @@ use app\security\oauth\providers\slack\Slack; use vhs\web\HttpServer; +/** @typescript */ class SlackOAuthHandler extends OAuthHandler { + /** + * getUrl. + * + * @return string + */ public function getUrl() { return '/oauth/slack.php'; } + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ public function handle(HttpServer $server) { $host = OauthHelper::redirectHost(); @@ -42,7 +55,7 @@ public function handle(HttpServer $server) { if (!isset($_GET['code'])) { $oauthHelper->requestAuth(['team' => OAUTH_SLACK_TEAM, 'user_scope' => 'identify']); } else { - /** @var SlackResourceOwner | null */ + /** @var \app\security\oauth\providers\slack\SlackUser */ $userDetails = $oauthHelper->processToken(); } diff --git a/packages/backend-php/app/security/oauth/providers/slack/Slack.php b/packages/backend-php/app/security/oauth/providers/slack/Slack.php new file mode 100644 index 00000000..94feea4a --- /dev/null +++ b/packages/backend-php/app/security/oauth/providers/slack/Slack.php @@ -0,0 +1,128 @@ +domain . '/api/oauth.v2.access'; + } + + /** + * getBaseAuthorizationUrl. + * + * @return string + */ + public function getBaseAuthorizationUrl() { + return $this->domain . '/oauth/v2/authorize'; + } + + /** + * getResourceOwnerDetailsUrl. + * + * @param \League\OAuth2\Client\Token\AccessToken $token + * + * @return string + */ + public function getResourceOwnerDetailsUrl(AccessToken $token) { + return $this->domain . '/api/auth.test?token=' . $token; + } + + /** + * checkResponse. + * + * @param \Psr\Http\Message\ResponseInterface $response + * @param array|string $data + * + * @throws \app\security\oauth\providers\slack\SlackProviderException + * + * @return void + * + * @phpstan-ignore missingType.iterableValue + */ + protected function checkResponse(ResponseInterface $response, $data) { + if (isset($data['ok']) && $data['ok'] !== true) { + SlackProviderException::fromResponse($response, $data['error']); + } + } + + /** + * createAccessToken. + * + * @param array $response + * @param \League\OAuth2\Client\Grant\AbstractGrant $grant + * + * @return \League\OAuth2\Client\Token\AccessToken + */ + protected function createAccessToken(array $response, AbstractGrant $grant) { + if (isset($response['authed_user'])) { + return new AccessToken($response['authed_user']); + } + + return new AccessToken($response['authed_user']); + } + + /** + * getDefaultScopes. + * + * @return string[] + */ + protected function getDefaultScopes() { + return []; + } +} diff --git a/packages/backend-php/app/security/oauth/providers/slack/SlackProviderException.php b/packages/backend-php/app/security/oauth/providers/slack/SlackProviderException.php new file mode 100644 index 00000000..ed4e8bde --- /dev/null +++ b/packages/backend-php/app/security/oauth/providers/slack/SlackProviderException.php @@ -0,0 +1,23 @@ +getStatusCode(), (string) $response->getBody()); + } +} diff --git a/packages/backend-php/app/security/oauth/providers/slack/SlackUser.php b/packages/backend-php/app/security/oauth/providers/slack/SlackUser.php new file mode 100644 index 00000000..b47513b6 --- /dev/null +++ b/packages/backend-php/app/security/oauth/providers/slack/SlackUser.php @@ -0,0 +1,53 @@ + + */ + protected $response; + + /** + * __construct. + * + * @param array $response + * + * @return void + */ + public function __construct(array $response) { + $this->response = $response; + } + + /** + * getId. + * + * @return mixed + */ + public function getId() { + return $this->response['user_id']; + } + + /** + * getName. + * + * @return string + */ + public function getName(): string { + return $this->response['user']; + } + + /** + * toArray. + * + * @return array + */ + public function toArray() { + return $this->response; + } +} diff --git a/app/services/ApiKeyService.php b/packages/backend-php/app/services/ApiKeyService.php similarity index 76% rename from app/services/ApiKeyService.php rename to packages/backend-php/app/services/ApiKeyService.php index 873d2d55..81fd115d 100644 --- a/app/services/ApiKeyService.php +++ b/packages/backend-php/app/services/ApiKeyService.php @@ -22,11 +22,14 @@ class ApiKeyService extends Service implements IApiKeyService1 { /** * @permission administrator|user * - * @param $keyid + * @param int $keyid * - * @return mixed + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return void */ public function DeleteApiKey($keyid) { + /** @var \app\domain\Key */ $key = Key::find($keyid); if (!CurrentUser::hasAnyPermissions('administrator') && $key->userid != CurrentUser::getIdentity()) { @@ -39,12 +42,13 @@ public function DeleteApiKey($keyid) { /** * @permission administrator * - * @param $notes + * @param string $notes * * @return mixed */ public function GenerateSystemApiKey($notes) { $apiKey = new Key(); + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); $apiKey->type = 'api'; $apiKey->notes = $notes; @@ -56,8 +60,11 @@ public function GenerateSystemApiKey($notes) { /** * @permission administrator|user * - * @param $userid - * @param $notes + * @param int $userid + * @param string $notes + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ @@ -73,11 +80,13 @@ public function GenerateUserApiKey($userid, $notes) { } $apiKey = new Key(); + $apiKey->key = bin2hex(openssl_random_pseudo_bytes(32)); $apiKey->type = 'api'; $apiKey->notes = $notes; $user->keys->add($apiKey); + $user->save(); return $apiKey; @@ -86,11 +95,15 @@ public function GenerateUserApiKey($userid, $notes) { /** * @permission administrator|user * - * @param $keyid + * @param int $keyid * - * @return mixed + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return \app\domain\Key */ public function GetApiKey($keyid) { + /** @var \app\domain\Key */ $key = Key::find($keyid); if (is_null($key)) { @@ -118,7 +131,9 @@ public function GetSystemApiKeys() { /** * @permission administrator|user * - * @param $userid + * @param int $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ @@ -133,19 +148,15 @@ public function GetUserApiKeys($userid) { /** * @permission administrator|user * - * @param $keyid - * @param $privileges + * @param int $keyid + * @param string|string[] $privileges * - * @return mixed + * @return void */ public function PutApiKeyPrivileges($keyid, $privileges) { $key = $this->GetApiKey($keyid); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -153,8 +164,10 @@ public function PutApiKeyPrivileges($keyid, $privileges) { $key->privileges->remove($priv); } - foreach ($privs as $priv) { - $key->privileges->add($priv); + if (!is_null($privs)) { + foreach ($privs as $priv) { + $key->privileges->add($priv); + } } $key->save(); @@ -163,11 +176,11 @@ public function PutApiKeyPrivileges($keyid, $privileges) { /** * @permission administrator|user * - * @param $keyid - * @param $notes - * @param $expires + * @param int $keyid + * @param string $notes + * @param string $expires * - * @return mixed + * @return void */ public function UpdateApiKey($keyid, $notes, $expires) { $key = $this->GetApiKey($keyid); diff --git a/app/services/AuthService.php b/packages/backend-php/app/services/AuthService.php similarity index 76% rename from app/services/AuthService.php rename to packages/backend-php/app/services/AuthService.php index e220a08c..fbdbf871 100644 --- a/app/services/AuthService.php +++ b/packages/backend-php/app/services/AuthService.php @@ -24,19 +24,47 @@ use vhs\database\Database; use vhs\database\queries\QuerySelect; use vhs\database\wheres\Where; +use vhs\domain\Domain; use vhs\domain\Filter; use vhs\security\CurrentUser; use vhs\security\exceptions\UnauthorizedException; use vhs\security\UserPassCredentials; use vhs\services\Service; +/** @typescript */ class AuthService extends Service implements IAuthService1 { + /** + * fill retVal from User result. + * + * @param \app\domain\Key &$key + * @param \app\domain\User &$user + * @param array &$retval + * + * @return bool + */ + private static function parseValidAccount(&$key, &$user, &$retval): bool { + if ($user->valid) { + $retval['valid'] = true; + $retval['userId'] = $user->id; + $retval['username'] = $user->username; + $retval['type'] = $user->membership->code; + $retval['privileges'] = $key->getAbsolutePrivileges(); + + return true; + } else { + $retval['username'] = $user->username; + $retval['message'] = $user->getInvalidReason(); + } + + return false; + } + /** * Check to see if the user pin and account is valid. * * @permission administrator|pin-auth * - * @param $pin + * @param string $pin * * @return mixed */ @@ -88,35 +116,30 @@ public function CheckPin($pin) { return $retval; } - // Fetch user + // Fetch userinfo $user = User::find($key->userid); - // Check if account is active and in good standing, and return result set - if ($user->valid) { - $retval['valid'] = true; - $retval['userId'] = $user->id; - $retval['username'] = $user->username; - $retval['type'] = $user->membership->code; - $retval['privileges'] = $key->getAbsolutePrivileges(); - - $logAccess(true, $user->id); + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); return $retval; - } else { - $retval['username'] = $user->username; - $retval['message'] = $user->getInvalidReason(); } - // Log and return - $logAccess(false, $user->id); + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + // Return return $retval; } /** * @permission administrator|rfid-auth * - * @param $rfid + * @param string $rfid * * @return mixed */ @@ -155,27 +178,23 @@ public function CheckRfid($rfid) { return $retval; } - // Fetch user info + // Fetch userinfo $user = User::find($key->userid); - // Check if account is active and in good standing, and return result set - if ($user->valid) { - $retval['valid'] = true; - $retval['userId'] = $user->id; - $retval['username'] = $user->username; - $retval['type'] = $user->membership->code; - $retval['privileges'] = $key->getAbsolutePrivileges(); - - $logAccess(true, $user->id); + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); return $retval; - } else { - $retval['username'] = $user->username; - $retval['message'] = $user->getInvalidReason(); } - $logAccess(false, $user->id); + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + // Return return $retval; } @@ -184,8 +203,8 @@ public function CheckRfid($rfid) { * * @permission administrator|service-auth * - * @param $service - * @param $id + * @param string $service + * @param string $id * * @return mixed */ @@ -230,34 +249,29 @@ public function CheckService($service, $id) { // Fetch userinfo $user = User::find($key->userid); - // Check if account is active and in good standing, and return result set - if ($user->valid) { - $retval['valid'] = true; - $retval['userId'] = $user->id; - $retval['username'] = $user->username; - $retval['type'] = $user->membership->code; - $retval['privileges'] = $key->getAbsolutePrivileges(); - - $logAccess(true, $user->id); + // Check if we have a user from the key + if ($user == null || !$user instanceof User) { + $logAccess(false); return $retval; - } else { - $retval['username'] = $user->username; - $retval['message'] = $user->getInvalidReason(); } - // Log and return - $logAccess(false, $user->id); + // Check if account is active and in good standing, and return result set + $isValid = self::parseValidAccount($key, $user, $retval); + + // Log + $logAccess($isValid, $user->id); + // Return return $retval; } /** * @permission anonymous * - * @param $username + * @param string $username * - * @return boolean + * @return bool */ public function CheckUsername($username) { return Database::exists( @@ -268,27 +282,18 @@ public function CheckUsername($username) { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ public function CountAccessLog($filters) { - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - return AccessLog::count($filters); } /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -299,8 +304,8 @@ public function CountClients($filters) { /** * @permission administrator|user * - * @param $userid - * @param $filters + * @param int $userid + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -313,8 +318,8 @@ public function CountUserAccessLog($userid, $filters) { /** * @permission administrator|user * - * @param $userid - * @param $filters + * @param int $userid + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -338,7 +343,7 @@ public function CurrentUser() { /** * @permission administrator|user * - * @param $id + * @param int $id * * @return mixed */ @@ -355,10 +360,10 @@ public function DeleteClient($id) { /** * @permission administrator|user * - * @param $id - * @param $enabled + * @param int $id + * @param bool $enabled * - * @return mixed + * @return void */ public function EnableClient($id, $enabled) { $client = $this->GetMyClient($id); @@ -375,7 +380,7 @@ public function EnableClient($id, $enabled) { /** * @permission oauth-provider * - * @param $bearerToken + * @param string $bearerToken * * @return mixed */ @@ -386,10 +391,10 @@ public function GetAccessToken($bearerToken) { /** * @permission anonymous * - * @param $clientId - * @param $clientSecret + * @param int $clientId + * @param string $clientSecret * - * @return mixed + * @return mixed|null */ public function GetClient($clientId, $clientSecret) { $client = AppClient::find($clientId); @@ -405,7 +410,7 @@ public function GetClient($clientId, $clientSecret) { * @permission oauth-provider * @permission authenticated * - * @param $clientId + * @param int $clientId * * @return mixed */ @@ -422,7 +427,7 @@ public function GetClientInfo($clientId) { /** * @permission oauth-provider * - * @param $refreshToken + * @param string $refreshToken * * @return mixed */ @@ -433,8 +438,8 @@ public function GetRefreshToken($refreshToken) { /** * @permission oauth-provider * - * @param $username - * @param $password + * @param string $username + * @param string $password * * @return mixed */ @@ -445,31 +450,26 @@ public function GetUser($username, $password) { /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * - * @return mixed + * @return \app\domain\AccessLog[] */ public function ListAccessLog($page, $size, $columns, $order, $filters) { - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } - return AccessLog::page($page, $size, $columns, $order, $filters); } /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -480,16 +480,14 @@ public function ListClients($page, $size, $columns, $order, $filters) { /** * @permission administrator|user * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed - * - * @throws \Exception */ public function ListUserAccessLog($userid, $page, $size, $columns, $order, $filters) { $filters = $this->AddUserIDToFilters($userid, $filters); @@ -500,25 +498,22 @@ public function ListUserAccessLog($userid, $page, $size, $columns, $order, $filt /** * @permission administrator|user * - * @param $userid - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $userid + * @param int $page + * @param int $size + * @param mixed $columns + * @param mixed $order + * @param string|\vhs\domain\Filter|null $filters * - * @return array + * @throws \vhs\security\exceptions\UnauthorizedException * - * @throws \Exception + * @return AppClient[] */ public function ListUserClients($userid, $page, $size, $columns, $order, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); @@ -544,8 +539,8 @@ public function ListUserClients($userid, $page, $size, $columns, $order, $filter /** * @permission anonymous * - * @param $username - * @param $password + * @param string $username + * @param string $password * * @return mixed */ @@ -562,7 +557,7 @@ public function Login($username, $password) { /** * @permission user * - * @return mixed + * @return void */ public function Logout() { Authenticate::getInstance()->logout(); @@ -571,9 +566,9 @@ public function Logout() { /** * @permission anonymous * - * @param $pin + * @param string $pin * - * @return mixed + * @return string */ public function PinLogin($pin) { try { @@ -588,10 +583,10 @@ public function PinLogin($pin) { /** * @permission user * - * @param $name - * @param $description - * @param $url - * @param $redirecturi + * @param string $name + * @param string $description + * @param string $url + * @param string $redirecturi * * @return mixed */ @@ -614,7 +609,7 @@ public function RegisterClient($name, $description, $url, $redirecturi) { /** * @permission oauth-provider * - * @param $refreshToken + * @param string $refreshToken * * @return mixed */ @@ -629,9 +624,9 @@ public function RevokeRefreshToken($refreshToken) { /** * @permission anonymous * - * @param $key + * @param string $key * - * @return mixed + * @return string */ public function RfidLogin($key) { try { @@ -646,10 +641,10 @@ public function RfidLogin($key) { /** * @permission oauth-provider * - * @param $userId - * @param $accessToken - * @param $clientId - * @param $expires + * @param int $userId + * @param string $accessToken + * @param int $clientId + * @param string $expires * * @return mixed */ @@ -670,7 +665,7 @@ public function SaveAccessToken($userId, $accessToken, $clientId, $expires) { $token->client = $client; } - $expiry = new \DateTime($expires); + $expiry = new DateTime($expires); $token->expires = $expiry->format('Y-m-d H:i:s'); @@ -682,10 +677,10 @@ public function SaveAccessToken($userId, $accessToken, $clientId, $expires) { /** * @permission oauth-provider * - * @param $userId - * @param $refreshToken - * @param $clientId - * @param $expires + * @param int $userId + * @param string $refreshToken + * @param int $clientId + * @param string $expires * * @return mixed */ @@ -706,7 +701,7 @@ public function SaveRefreshToken($userId, $refreshToken, $clientId, $expires) { $token->client = $client; } - $expiry = new \DateTime($expires); + $expiry = new DateTime($expires); $token->expires = $expiry->format('Y-m-d H:i:s'); @@ -715,14 +710,21 @@ public function SaveRefreshToken($userId, $refreshToken, $clientId, $expires) { return $this->trimUser($user); } + /** + * AddUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ private function AddUserIDToFilters($userid, $filters) { $userService = new UserService(); $user = $userService->GetUser($userid); - if (is_string($filters)) { - //todo total hack.. this is to support GET params for downloading payments - $filters = json_decode($filters); - } + Domain::coerceFilters($filters); if (is_null($user)) { throw new UnauthorizedException('User not found or you do not have access'); @@ -739,6 +741,13 @@ private function AddUserIDToFilters($userid, $filters) { return $filters; } + /** + * GetMyClient. + * + * @param int $id + * + * @return AppClient|null + */ private function GetMyClient($id) { $client = AppClient::find($id); @@ -753,6 +762,13 @@ private function GetMyClient($id) { return null; } + /** + * trimClient. + * + * @param AppClient $client + * + * @return array|null + */ private function trimClient($client) { if (is_null($client)) { return null; @@ -770,6 +786,13 @@ private function trimClient($client) { ]; } + /** + * trimClientInfo. + * + * @param AppClient|null $client + * + * @return array|null + */ private function trimClientInfo($client) { if (is_null($client)) { return null; @@ -783,6 +806,13 @@ private function trimClientInfo($client) { ]; } + /** + * trimUser. + * + * @param User|null $user + * + * @return array|null + */ private function trimUser($user) { if (is_null($user)) { return null; diff --git a/packages/backend-php/app/services/EmailService.php b/packages/backend-php/app/services/EmailService.php new file mode 100644 index 00000000..2e7fee46 --- /dev/null +++ b/packages/backend-php/app/services/EmailService.php @@ -0,0 +1,266 @@ +delete(); + } + } + + /** + * Email. + * + * @param mixed $email + * @param mixed $tmpl + * @param mixed $context + * @param mixed $subject + * + * @return null + */ + public function Email($email, $tmpl, $context, $subject = null) { + $generated = EmailTemplate::generate($tmpl, $context); + + if (is_null($generated)) { + throw new \Exception('Unable to load e-mail template'); + } + + if (is_null($subject)) { + $subject = $generated->subject; + } + + $client = new SesClient([ + 'region' => AWS_SES_REGION, + 'credentials' => [ + 'key' => AWS_SES_CLIENT_ID, + 'secret' => AWS_SES_SECRET + ] + ]); + + $client->sendEmail([ + 'Source' => NOMOS_FROM_EMAIL, + 'Destination' => [ + 'ToAddresses' => [$email] + ], + 'Message' => [ + 'Subject' => [ + // Data is required + 'Data' => $subject + ], + // Body is required + 'Body' => [ + 'Text' => [ + // Data is required + 'Data' => $generated->txt + ], + 'Html' => [ + // Data is required + 'Data' => $generated->html + ] + ] + ] + ]); + + return null; + } + + /** + * EmailUser. + * + * @param mixed $user + * @param mixed $tmpl + * @param mixed $context + * @param mixed $subject + * + * @return void + */ + public function EmailUser($user, $tmpl, $context, $subject = null) { + $this->Email($user->email, $tmpl, $context, $subject); + } + + /** + * @permission administrator + * + * @param int $id + * + * @return \app\domain\EmailTemplate + */ + public function GetTemplate($id) { + /** @var \app\domain\EmailTemplate */ + return EmailTemplate::find($id); + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return \app\domain\EmailTemplate[] + */ + public function ListTemplates($page, $size, $columns, $order, $filters) { + /** @var \app\domain\EmailTemplate[] */ + return EmailTemplate::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $name + * @param string $code + * @param string $subject + * @param string $help + * @param string $body + * @param string $html + * + * @return void + */ + public function PutTemplate($name, $code, $subject, $help, $body, $html) { + $template = EmailTemplate::findByCode($code); + + if (is_null($template)) { + $template = new EmailTemplate(); + } + + $template->name = $name; + $template->code = $code; + $template->subject = $subject; + $template->help = $help; + $template->body = $body; + $template->html = $html; + + $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $body + * + * @return void + */ + public function UpdateTemplateBody($id, $body) { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->body = $body; + + $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $code + * + * @return void + */ + public function UpdateTemplateCode($id, $code) { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->code = $code; + + $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $help + * + * @return void + */ + public function UpdateTemplateHelp($id, $help) { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->help = $help; + + $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $html + * + * @return void + */ + public function UpdateTemplateHtml($id, $html) { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->html = $html; + + $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $name + * + * @return void + */ + public function UpdateTemplateName($id, $name) { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->name = $name; + + $template->save(); + } + + /** + * @permission administrator + * + * @param int $id + * @param string $subject + * + * @return void + */ + public function UpdateTemplateSubject($id, $subject) { + /** @var \app\domain\EmailTemplate */ + $template = EmailTemplate::find($id); + + $template->subject = $subject; + + $template->save(); + } +} diff --git a/app/services/EventService.php b/packages/backend-php/app/services/EventService.php similarity index 80% rename from app/services/EventService.php rename to packages/backend-php/app/services/EventService.php index 65b77329..d7baa297 100644 --- a/app/services/EventService.php +++ b/packages/backend-php/app/services/EventService.php @@ -13,16 +13,15 @@ use app\domain\Event; use app\domain\Privilege; use app\exceptions\InvalidInputException; -use Aws\CloudFront\Exception\Exception; -use vhs\domain\Domain; use vhs\security\CurrentUser; use vhs\services\Service; +/** @typescript */ class EventService extends Service implements IEventService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return int */ @@ -33,11 +32,13 @@ public function CountEvents($filters) { /** * @permission administrator * - * @param $name - * @param $domain - * @param $event - * @param $description - * @param $enabled + * @param string $name + * @param string $domain + * @param string $event + * @param string $description + * @param bool $enabled + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ @@ -60,7 +61,7 @@ public function CreateEvent($name, $domain, $event, $description, $enabled) { /** * @permission administrator * - * @param $id + * @param int $id * * @return mixed */ @@ -73,8 +74,8 @@ public function DeleteEvent($id) { /** * @permission administrator * - * @param $id - * @param $enabled + * @param int $id + * @param bool $enabled * * @return mixed */ @@ -117,7 +118,7 @@ public function GetAccessibleEvents() { /** * @permission webhook|administrator * - * @param $domain + * @param string $domain * * @return mixed */ @@ -151,7 +152,9 @@ public function GetDomainDefinitions() { /** * @permission administrator * - * @param $id + * @param int $id + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ @@ -186,17 +189,17 @@ public function GetEventTypes() { sort($updateKeys); - return array_map(fn($method): string => str_replace('before', 'before:', strtolower(str_replace('onAny', '', $method))), $updateKeys); + return array_map(fn ($method): string => str_replace('before', 'before:', strtolower(str_replace('onAny', '', $method))), $updateKeys); } /** * @permission webhook|administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -207,19 +210,15 @@ public function ListEvents($page, $size, $columns, $order, $filters) { /** * @permission administrator * - * @param $id - * @param $privileges + * @param int $id + * @param string $privileges * - * @return mixed + * @return void */ public function PutEventPrivileges($id, $privileges) { $event = $this->GetEvent($id); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -237,12 +236,12 @@ public function PutEventPrivileges($id, $privileges) { /** * @permission administrator * - * @param $id - * @param $name - * @param $domain - * @param $event - * @param $description - * @param $enabled + * @param int $id + * @param string $name + * @param string $domain + * @param string $event + * @param string $description + * @param bool $enabled * * @return mixed */ diff --git a/packages/backend-php/app/services/IpnService.php b/packages/backend-php/app/services/IpnService.php new file mode 100644 index 00000000..df980676 --- /dev/null +++ b/packages/backend-php/app/services/IpnService.php @@ -0,0 +1,55 @@ +key = $value; + break; case 'pin': - $nextpinid = Database::scalar(Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid)); + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), SettingsSchema::Columns()->nextpinid) + ); $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + // TODO fix typing + /** @disregard P1006 override */ $key->privileges->add(Privilege::findByCode('inherit')); + break; case 'api': $key->key = bin2hex(openssl_random_pseudo_bytes(32)); + break; default: throw new InvalidInputException('Unsupported key type'); @@ -85,6 +97,8 @@ public function GenerateUserKey($userid, $type, $value, $notes) { /** * @permission administrator * + * @throws \vhs\security\exceptions\UnauthorizedException + * * @return mixed */ public function GetAllKeys() { @@ -98,11 +112,15 @@ public function GetAllKeys() { /** * @permission administrator|user * - * @param $keyid + * @param int $keyid + * + * @throws \app\exceptions\InvalidInputException + * @throws \vhs\security\exceptions\UnauthorizedException * * @return mixed */ public function GetKey($keyid) { + /** @var \app\domain\Key */ $key = Key::find($keyid); if (is_null($key)) { @@ -121,6 +139,8 @@ public function GetKey($keyid) { /** * @permission administrator * + * @throws \vhs\security\exceptions\UnauthorizedException + * * @return mixed */ public function GetSystemKeys() { @@ -128,14 +148,16 @@ public function GetSystemKeys() { throw new UnauthorizedException(); } + // TODO implement proper typing + // @phpstan-ignore property.notFound return Key::where(Where::Null(Key::Schema()->Columns()->userid)); } /** * @permission administrator|user * - * @param $userid - * @param $types + * @param int $userid + * @param $types * * @return mixed */ @@ -160,20 +182,15 @@ public function GetUserKeys($userid, $types) { /** * @permission administrator|user * - * @param $keyid - * @param $privileges + * @param int $keyid + * @param $privileges * * @return mixed */ public function PutKeyPrivileges($keyid, $privileges) { $key = $this->GetKey($keyid); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = []; - array_push($privArray, $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -191,9 +208,9 @@ public function PutKeyPrivileges($keyid, $privileges) { /** * @permission administrator|user * - * @param $keyid - * @param $notes - * @param $expires + * @param int $keyid + * @param $notes + * @param $expires * * @return mixed */ diff --git a/packages/backend-php/app/services/MemberCardService.php b/packages/backend-php/app/services/MemberCardService.php new file mode 100644 index 00000000..a45f7afd --- /dev/null +++ b/packages/backend-php/app/services/MemberCardService.php @@ -0,0 +1,284 @@ +addUserIDToFilters($userid, $filters); + + return GenuineCard::count($filters); + } + + /** + * @permission administrator + * + * @param string $key + * + * @return mixed + */ + public function GetGenuineCardDetails($key) { + return GenuineCard::findByKey($key)[0]; + } + + /** + * @permission administrator + * + * @param string $email + * @param string $key + * + * @throws \app\exceptions\InvalidInputException + * @throws \app\exceptions\MemberCardException + * + * @return mixed + */ + public function IssueCard($email, $key) { + $users = User::findByPaymentEmail($email); + + if (is_null($users) || count($users) != 1) { + throw new InvalidInputException('Invalid email address'); + } + + if (!$this->ValidateGenuineCard($key)) { + throw new InvalidInputException('Invalid card'); + } + + $user = $users[0]; + $card = GenuineCard::findByKey($key)[0]; + + $payments = Payment::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO eventually put these into card campaigns or something + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->item_number, 'vhs_card_2015'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->payer_email, $email), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Payment::Schema()->Columns()->user_id, $user->id), + Where::NotIn( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Payment::Schema()->Columns()->id, + Query::Select( + GenuineCard::Schema()->Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + new Columns(GenuineCard::Schema()->Columns()->paymentid) + ) + ) + ) + ); + + if (is_null($payments) || count($payments) < 1) { + throw new MemberCardException('User has not paid for a member card.'); + } + + $payment = $payments[0]; + + $card->paymentid = $payment->id; + $card->active = true; + $card->userid = $user->id; + $card->owneremail = $email; + $card->issued = date('Y-m-d H:i:s'); + $card->notes = 'Issued by admin to ' . $user->fname . ' ' . $user->lname; + + $card->save(); + + $keyService = new KeyService(); + + $keyService->GenerateUserKey($user->id, 'rfid', $key, 'Genuine VHS Membership Card'); + + return $card; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return mixed + */ + public function ListGenuineCards($page, $size, $columns, $order, $filters) { + return GenuineCard::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return mixed + */ + public function ListUserGenuineCards($userid, $page, $size, $columns, $order, $filters) { + $userService = new UserService(); + $user = $userService->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::_Or(Filter::Equal('userid', $user->id), Filter::Equal('owneremail', $user->email)); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return GenuineCard::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param string $key + * @param string $notes + * + * @throws \app\exceptions\MemberCardException + * + * @return GenuineCard + */ + public function RegisterGenuineCard($key, $notes) { + $keys = GenuineCard::findByKey($key); + + if (!is_null($keys) && count($keys) != 0) { + //card already registered + throw new MemberCardException('Failed to register card'); + } + + $card = new GenuineCard(); + + $card->key = $key; + + $card->save(); + + return $card; + } + + /** + * @permission administrator + * + * @param string $key + * @param bool $active + * + * @throws \app\exceptions\InvalidInputException + * + * @return mixed + */ + public function UpdateGenuineCardActive($key, $active) { + if (!$this->ValidateGenuineCard($key)) { + throw new InvalidInputException('Invalid card'); + } + + $card = GenuineCard::findByKey($key)[0]; + + $card->active = $active; + + $card->save(); + + return $card; + } + + /** + * @permission user + * + * @param string $key + * + * @return bool + */ + public function ValidateGenuineCard($key) { + $keys = GenuineCard::findByKey($key); + + return !is_null($keys) && count($keys) == 1; + } + + /** + * addUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ + private function addUserIDToFilters($userid, $filters) { + $userService = new UserService(); + $user = $userService->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } +} diff --git a/packages/backend-php/app/services/MembershipService.php b/packages/backend-php/app/services/MembershipService.php new file mode 100644 index 00000000..9a629903 --- /dev/null +++ b/packages/backend-php/app/services/MembershipService.php @@ -0,0 +1,197 @@ +Get($membershipId); + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($membership->privileges->all() as $priv) { + $membership->privileges->remove($priv); + } + + if (!empty($privs)) { + foreach ($privs as $priv) { + $membership->privileges->add($priv); + } + } + + $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param string $title + * @param string $description + * @param float $price + * @param string $code + * @param int $days + * @param string $period + * + * @return mixed + */ + public function Update($membershipId, $title, $description, $price, $code, $days, $period) { + $membership = $this->Get($membershipId); + + $membership->title = $title; + $membership->description = $description; + $membership->price = $price; + $membership->code = $code; + $membership->days = $days; + $membership->period = $period; + + $membership->save(); + + return $membership; + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $active + * + * @return void + */ + public function UpdateActive($membershipId, $active) { + $membership = $this->Get($membershipId); + + $membership->active = $active; + + $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $private + * + * @return void + */ + public function UpdatePrivate($membershipId, $private) { + $membership = $this->Get($membershipId); + + $membership->private = $private; + + $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $recurring + * + * @return void + */ + public function UpdateRecurring($membershipId, $recurring) { + $membership = $this->Get($membershipId); + + $membership->recurring = $recurring; + + $membership->save(); + } + + /** + * @permission administrator + * + * @param int $membershipId + * @param bool $trial + * + * @return void + */ + public function UpdateTrial($membershipId, $trial) { + $membership = $this->Get($membershipId); + + $membership->trial = $trial; + + $membership->save(); + } +} diff --git a/app/services/MetricService.php b/packages/backend-php/app/services/MetricService.php similarity index 76% rename from app/services/MetricService.php rename to packages/backend-php/app/services/MetricService.php index 32ac07d3..16822ce7 100644 --- a/app/services/MetricService.php +++ b/packages/backend-php/app/services/MetricService.php @@ -13,12 +13,13 @@ use vhs\database\wheres\Where; use vhs\services\Service; +/** @typescript */ class MetricService extends Service implements IMetricService1 { /** * Get the total new members recorded in the date range. * - * @param $start int unixtime - * @param $end int unixtime + * @param int $start int unixtime + * @param int $end int unixtime * * @return int */ @@ -26,9 +27,17 @@ protected static function NewMemberCount($start, $end) { $query = Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $start)) ) ); @@ -39,9 +48,9 @@ protected static function NewMemberCount($start, $end) { /** * Get the total new memberships of a type recorded in the date range. * - * @param $membership_id int - * @param $start int unixtime - * @param $end int unixtime + * @param int $membership_id int + * @param int $start int unixtime + * @param int $end int unixtime * * @return int */ @@ -49,10 +58,20 @@ protected static function NewMembershipByIdCount($membership_id, $start, $end) { $query = Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->membership_id, $membership_id), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $end)), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->created, date('Y-m-d 00:00:00', $start)) ) ); @@ -63,9 +82,6 @@ protected static function NewMembershipByIdCount($membership_id, $start, $end) { /** * Get the total members. * - * @param $start int unixtime - * @param $end int unixtime - * * @return int */ protected static function TotalMemberCount() { @@ -73,20 +89,37 @@ protected static function TotalMemberCount() { Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')) ) ) ); } + /** + * TotalMembershipByIdCount. + * + * @param int $membership_id + * + * @return int + */ protected static function TotalMembershipByIdCount($membership_id) { return Database::count( Query::count( UserSchema::Table(), Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->active, 'y'), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(UserSchema::Columns()->mem_expire, date('Y-m-d H:i:s')), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(UserSchema::Columns()->membership_id, $membership_id) ) ) @@ -96,15 +129,19 @@ protected static function TotalMembershipByIdCount($membership_id) { /** * @permission user * - * @param $start_range - * @param $end_range + * @param string $start_range + * @param string $end_range * * @return mixed */ public function GetCreatedDates($start_range, $end_range) { $users = User::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(User::Schema()->Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(User::Schema()->Columns()->created, $end_range) ) ); @@ -161,31 +198,45 @@ public function GetCreatedDates($start_range, $end_range) { * @return mixed */ public function GetExceptionPayments() { + // TODO implement proper typing + // @phpstan-ignore property.notFound return Payment::where(Where::NotEqual(Payment::Schema()->Columns()->status, 1)); } /** * @permission user * - * @param $start_range - * @param $end_range - * @param $group + * @param string $start_range + * @param string $end_range + * @param string $group * * @return mixed */ public function GetMembers($start_range, $end_range, $group) { $users = User::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(User::Schema()->Columns()->created, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(User::Schema()->Columns()->created, $end_range) ) ); $payments = Payment::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Like(Payment::Schema()->Columns()->item_number, 'vhs_membership_%') ) ); @@ -266,6 +317,8 @@ public function GetNewMembers($start_range, $end_range) { * @return mixed */ public function GetPendingAccounts() { + // TODO implement proper typing + // @phpstan-ignore property.notFound return User::where(Where::Equal(User::Schema()->Columns()->active, 't')); } @@ -281,8 +334,14 @@ public function GetPendingAccounts() { public function GetRevenue($start_range, $end_range, $group) { $payments = Payment::where( Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::Equal(Payment::Schema()->Columns()->status, 1), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::GreaterEqual(Payment::Schema()->Columns()->date, $start_range), + // TODO implement proper typing + // @phpstan-ignore property.notFound Where::LesserEqual(Payment::Schema()->Columns()->date, $end_range) ) ); @@ -301,15 +360,19 @@ public function GetRevenue($start_range, $end_range, $group) { switch ($group) { case 'day': $grouping = $grouping->format('Y-m-d'); + break; case 'month': $grouping = $grouping->format('Y-m'); + break; case 'year': $grouping = $grouping->format('Y'); + break; default: $grouping = 'all'; + break; } @@ -361,6 +424,15 @@ public function GetTotalMembers() { ]; } + /** + * countByDate. + * + * @param mixed[] $arr + * @param mixed $date + * @param mixed $group + * + * @return mixed[] + */ private function countByDate($arr, $date, $group) { if (is_null($date)) { return $arr; @@ -375,15 +447,19 @@ private function countByDate($arr, $date, $group) { switch ($group) { case 'day': $grouping = $grouping->format('Y-m-d'); + break; case 'month': $grouping = $grouping->format('Y-m'); + break; case 'year': $grouping = $grouping->format('Y'); + break; default: $grouping = 'all'; + break; } diff --git a/packages/backend-php/app/services/PaymentService.php b/packages/backend-php/app/services/PaymentService.php new file mode 100644 index 00000000..e3c6f9b8 --- /dev/null +++ b/packages/backend-php/app/services/PaymentService.php @@ -0,0 +1,154 @@ +AddUserIDOrEMailToFilters($userid, $filters); + + return Payment::count($filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return mixed + */ + public function GetPayment($id) { + /** @var \app\domain\Payment */ + $payment = Payment::find($id); + + if (is_null($payment)) { + return null; + } + + if (CurrentUser::getIdentity() == $payment->user_id || CurrentUser::hasAnyPermissions('administrator')) { + return $payment; + } + + return null; + } + + /** + * @permission administrator + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return mixed + */ + public function ListPayments($page, $size, $columns, $order, $filters) { + return Payment::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return mixed + */ + public function ListUserPayments($userid, $page, $size, $columns, $order, $filters) { + $filters = $this->AddUserIDOrEMailToFilters($userid, $filters); + + return Payment::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator + * + * @param int $paymentid + * + * @return mixed + */ + public function ReplayPaymentProcessing($paymentid) { + /** @var StringLogger */ + $log = new StringLogger(); + + $log->log('Attempting a reply of payment id: ' . $paymentid); + + $processor = new PaymentProcessor($log); + + try { + $processor->paymentCreated($paymentid); + } catch (\Exception $ex) { + $log->log('Exception: ' . $ex->getMessage()); + $log->log($ex->getTraceAsString()); + } + + $log->log('Replay complete.'); + + // @phpstan-ignore method.notFound + return $log->fullText(); + } + + /** + * AddUserIDOrEMailToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ + private function AddUserIDOrEMailToFilters($userid, $filters) { + $userService = new UserService(); + $user = $userService->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::_Or(Filter::Equal('user_id', $user->id), Filter::Equal('payer_email', $user->email)); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } +} diff --git a/packages/backend-php/app/services/PinService.php b/packages/backend-php/app/services/PinService.php new file mode 100644 index 00000000..6a2cba2e --- /dev/null +++ b/packages/backend-php/app/services/PinService.php @@ -0,0 +1,221 @@ +GetUserPin($userid); + + if (is_null($pin)) { + $nextpinid = Database::scalar( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Query::Select(SettingsSchema::Table(), new Columns(SettingsSchema::Columns()->nextpinid)) + ); + + $key = new Key(); + $key->userid = $userid; + $key->type = 'pin'; + $key->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + $key->notes = 'User generated PIN'; + + $pin = $key; + + $priv = Privilege::findByCode('inherit'); + if (!is_null($priv)) { + // TODO fix typing + /** @disregard P1006 override */ + $pin->privileges->add($priv); + } + } + + $pinid = explode('|', $pin->key)[0]; + + $pin->key = sprintf('%04s', $pinid) . '|' . sprintf('%04s', rand(0, 9999)); + $pin->notes = 'User generated PIN'; + + $pin->save(); + + return $pin; + } + + /** + * @permission gen-temp-pin|administrator + * + * @param string $expires + * @param string $privileges + * @param string $notes + * + * @return mixed + */ + public function GenerateTemporaryPin($expires, $privileges, $notes) { + $userid = CurrentUser::getIdentity(); + + $nextpinid = Database::scalar( + Query::Select( + SettingsSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + new Columns(SettingsSchema::Columns()->nextpinid) + ) + ); + + $pin = new Key(); + $pin->userid = $userid; + $pin->expires = $expires; + $pin->type = 'pin'; + $pin->key = sprintf('%04s', $nextpinid) . '|' . sprintf('%04s', rand(0, 9999)); + $pin->notes = $notes; + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + if (!is_null($privs) && is_array($privs)) { + foreach ($privs as $priv) { + if (CurrentUser::hasAllPermissions($priv->code)) { + // TODO fix typing + /** @disregard P1006 override */ + $pin->privileges->add($priv); + } + } + } + + $pin->save(); + + return $pin; + } + + /** + * @permission administrator|user + * + * @param int $userid + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return mixed + */ + public function GetUserPin($userid) { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $keys = Key::where( + Where::_And( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->type, 'pin'), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(Key::Schema()->Columns()->userid, $userid) + ) + ); + + if (count($keys) >= 1) { + return $keys[0]; + } + + return null; + } + + /** + * @permission administrator|user + * + * @param int $keyid + * @param string $pin + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return void + */ + public function UpdatePin($keyid, $pin) { + /** @var \app\domain\Key */ + $key = Key::find($keyid); + + if (!CurrentUser::hasAnyPermissions('administrator') && $key->userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $pinid = explode('|', $key->key)[0]; + + $key->key = $pinid . '|' . sprintf('%04s', intval($pin)); + + $key->save(); + } + + /** + * Change a pin. + * + * @permission administrator|user + * + * @param int $userid + * @param string $pin + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return mixed + */ + public function UpdateUserPin($userid, $pin) { + if (!CurrentUser::hasAnyPermissions('administrator') && $userid != CurrentUser::getIdentity()) { + throw new UnauthorizedException(); + } + + $pinObj = $this->GetUserPin($userid); + + if (is_null($pin)) { + $pinObj = $this->GeneratePin($userid); + } + + $pinid = explode('|', $pinObj->key)[0]; + + $pinObj->key = $pinid . '|' . $pin; + + $pinObj->save(); + + return $pinObj; + } +} diff --git a/app/services/PreferenceService.php b/packages/backend-php/app/services/PreferenceService.php similarity index 75% rename from app/services/PreferenceService.php rename to packages/backend-php/app/services/PreferenceService.php index c85ba5fe..befda953 100644 --- a/app/services/PreferenceService.php +++ b/packages/backend-php/app/services/PreferenceService.php @@ -15,13 +15,14 @@ use vhs\security\CurrentUser; use vhs\services\Service; +/** @typescript */ class PreferenceService extends Service implements IPreferenceService1 { /** * @permission administrator * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * - * @return array + * @return int */ public function CountSystemPreferences($filters) { return SystemPreference::count($filters); @@ -30,11 +31,12 @@ public function CountSystemPreferences($filters) { /** * @permission administrator * - * @param $key + * @param string $key * * @return mixed */ public function DeleteSystemPreference($key) { + /** @var \app\domain\SystemPreference[] */ $prefs = SystemPreference::findByKey($key); if (is_null($prefs) || count($prefs) <= 0) { @@ -58,34 +60,38 @@ public function GetAllSystemPreferences() { /** * @permission administrator * - * @param $key + * @param int $id * * @return mixed */ public function GetSystemPreference($id) { + /** @var \app\domain\SystemPreference */ return SystemPreference::find($id); } /** * @permission administrator * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param mixed $columns + * @param mixed $order + * @param string|\vhs\domain\Filter|null $filters * - * @return array + * @return \app\domain\SystemPreference[] */ public function ListSystemPreferences($page, $size, $columns, $order, $filters) { + /** @var \app\domain\SystemPreference[] */ return SystemPreference::page($page, $size, $columns, $order, $filters); } /** * @permission administrator * - * @param $key - * @param $value + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes * * @return mixed */ @@ -113,23 +119,20 @@ public function PutSystemPreference($key, $value, $enabled, $notes) { /** * @permission administrator * - * @param $id - * @param $privileges + * @param int $id + * @param string $privileges * * @return mixed */ public function PutSystemPreferencePrivileges($id, $privileges) { + /** @var \app\domain\SystemPreference */ $pref = SystemPreference::find($id); if (is_null($pref)) { return; } - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); @@ -147,11 +150,12 @@ public function PutSystemPreferencePrivileges($id, $privileges) { /** * @permission anonymous * - * @param $key + * @param string $key * * @return mixed */ public function SystemPreference($key) { + /** @var \app\domain\SystemPreference[] */ $prefs = SystemPreference::findByKey($key, function ($privileges) { $codes = []; foreach ($privileges->all() as $priv) { @@ -171,13 +175,16 @@ public function SystemPreference($key) { /** * @permission administrator * - * @param $id - * @param $key - * @param $value + * @param int $id + * @param string $key + * @param string $value + * @param bool $enabled + * @param string $notes * * @return mixed */ public function UpdateSystemPreference($id, $key, $value, $enabled, $notes) { + /** @var \app\domain\SystemPreference */ $pref = SystemPreference::find($id); if (is_null($pref)) { @@ -197,12 +204,13 @@ public function UpdateSystemPreference($id, $key, $value, $enabled, $notes) { /** * @permission administrator * - * @param $key - * @param $enabled + * @param string $key + * @param bool $enabled * * @return mixed */ public function UpdateSystemPreferenceEnabled($key, $enabled) { + /** @var \app\domain\SystemPreference[] */ $prefs = SystemPreference::findByKey($key); $pref = null; diff --git a/app/services/PrivilegeService.php b/packages/backend-php/app/services/PrivilegeService.php similarity index 78% rename from app/services/PrivilegeService.php rename to packages/backend-php/app/services/PrivilegeService.php index f491ddb0..df246ea5 100644 --- a/app/services/PrivilegeService.php +++ b/packages/backend-php/app/services/PrivilegeService.php @@ -12,8 +12,6 @@ use app\contracts\IPrivilegeService1; use app\domain\Privilege; use app\exceptions\InvalidInputException; -use app\exceptions\MemberCardException; -use vhs\security\exceptions\UnauthorizedException; use vhs\services\endpoints\Endpoint; use vhs\services\Service; use vhs\services\ServiceRegistry; @@ -22,7 +20,7 @@ class PrivilegeService extends Service implements IPrivilegeService1 { /** * @permission administrator|user|grants * - * @param $filters + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -33,11 +31,13 @@ public function CountPrivileges($filters) { /** * @permission administrator * - * @param $name - * @param $code - * @param $description - * @param $icon - * @param $enabled + * @param string $name + * @param string $code + * @param string $description + * @param string $icon + * @param bool $enabled + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ @@ -64,9 +64,9 @@ public function CreatePrivilege($name, $code, $description, $icon, $enabled) { /** * @permission administrator * - * @param $id + * @param int $id * - * @return mixed + * @return void */ public function DeletePrivilege($id) { $priv = Privilege::find($id); @@ -77,9 +77,10 @@ public function DeletePrivilege($id) { /** * @permission administrator|user|grants * - * @return mixed + * @return \app\domain\Privilege[] */ public function GetAllPrivileges() { + /** @var \app\domain\Privilege[] */ return Privilege::findAll(); } @@ -116,18 +117,19 @@ public function GetAllSystemPermissions() { /** * @permission user * - * @param $id + * @param int $id * - * @return mixed + * @return \app\domain\Privilege */ public function GetPrivilege($id) { + /** @var \app\domain\Privilege */ return Privilege::find($id); } /** * @permission administrator|user * - * @param $userid + * @param int $userid * * @return mixed */ @@ -153,11 +155,11 @@ public function GetUserPrivileges($userid) { /** * @permission administrator|user|grants * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -168,12 +170,13 @@ public function ListPrivileges($page, $size, $columns, $order, $filters) { /** * @permission administrator * - * @param $id - * @param $description + * @param int $id + * @param string $description * * @return mixed */ public function UpdatePrivilegeDescription($id, $description) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->description = $description; @@ -184,12 +187,13 @@ public function UpdatePrivilegeDescription($id, $description) { /** * @permission administrator * - * @param $id - * @param $enabled + * @param int $id + * @param bool $enabled * * @return mixed */ public function UpdatePrivilegeEnabled($id, $enabled) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->enabled = $enabled; @@ -200,12 +204,13 @@ public function UpdatePrivilegeEnabled($id, $enabled) { /** * @permission administrator * - * @param $id - * @param $icon + * @param int $id + * @param string $icon * * @return mixed */ public function UpdatePrivilegeIcon($id, $icon) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->icon = $icon; @@ -216,12 +221,13 @@ public function UpdatePrivilegeIcon($id, $icon) { /** * @permission administrator * - * @param $id - * @param $name + * @param int $id + * @param string $name * * @return mixed */ public function UpdatePrivilegeName($id, $name) { + /** @var \app\domain\Privilege */ $priv = Privilege::find($id); $priv->name = $name; diff --git a/packages/backend-php/app/services/StripeEventService.php b/packages/backend-php/app/services/StripeEventService.php new file mode 100644 index 00000000..c6224a24 --- /dev/null +++ b/packages/backend-php/app/services/StripeEventService.php @@ -0,0 +1,61 @@ +stripe_email = $email; $user->fname = $fname; $user->lname = $lname; - $user->active = 't'; + $user->active = UserActiveEnum::PENDING->value; $user->token = bin2hex(openssl_random_pseudo_bytes(8)); $user->save(); @@ -98,7 +103,7 @@ public function Create($username, $password, $email, $fname, $lname, $membership /** * @permission grants * - * @param $userid + * @param int $userid * * @return mixed */ @@ -122,7 +127,7 @@ public function GetGrantUserPrivileges($userid) { /** * @permission user|administrator * - * @param $userid + * @param int $userid * * @return mixed */ @@ -153,7 +158,7 @@ public function GetStatuses() { /** * @permission administrator|user * - * @param $userid + * @param int $userid * * @return mixed */ @@ -177,8 +182,8 @@ public function GetUsers() { /** * @permission grants * - * @param $userid - * @param $privilege + * @param int $userid + * @param string $privilege * * @return mixed */ @@ -200,12 +205,16 @@ public function GrantPrivilege($userid, $privilege) { return; } + // TODO fix typing + /** @disregard P1006 override */ foreach ($user->privileges->all() as $p) { if ($p->code == $priv->code) { return; } } + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->add($priv); $user->save(); } @@ -213,11 +222,11 @@ public function GrantPrivilege($userid, $privilege) { /** * @permission administrator|grants * - * @param $page - * @param $size - * @param $columns - * @param $order - * @param $filters + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters * * @return mixed */ @@ -228,25 +237,29 @@ public function ListUsers($page, $size, $columns, $order, $filters) { /** * @permission administrator * - * @param $userid - * @param $privileges + * @param int $userid + * @param string $privileges + * + * @return void */ public function PutUserPrivileges($userid, $privileges) { $user = User::find($userid); - $privArray = $privileges; - - if (!is_array($privArray)) { - $privArray = explode(',', $privileges); - } + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; $privs = Privilege::findByCodes(...$privArray); + // TODO fix typing + /** @disregard P1006 override */ foreach ($user->privileges->all() as $priv) { + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->remove($priv); } foreach ($privs as $priv) { + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->add($priv); } @@ -256,11 +269,14 @@ public function PutUserPrivileges($userid, $privileges) { /** * @permission anonymous * - * @param $username - * @param $password - * @param $email - * @param $fname - * @param $lname + * @param string $username + * @param string $password + * @param string $email + * @param string $fname + * @param string $lname + * + * @throws \app\exceptions\InvalidPasswordHashException + * @throws \app\exceptions\UserAlreadyExistsException * * @return mixed */ @@ -282,7 +298,7 @@ public function Register($username, $password, $email, $fname, $lname) { $user->email = $email; $user->fname = $fname; $user->lname = $lname; - $user->active = 't'; + $user->active = UserActiveEnum::PENDING->value; $user->token = bin2hex(openssl_random_pseudo_bytes(8)); $user->save(); @@ -306,7 +322,7 @@ public function Register($username, $password, $email, $fname, $lname) { /** * @permission anonymous * - * @param $email + * @param string $email * * @return mixed */ @@ -341,19 +357,19 @@ public function RequestPasswordReset($email) { /** * @permission user * - * @param $email + * @param string $email * * @return mixed */ public function RequestSlackInvite($email) { $ch = curl_init('http://slack-invite:3000/invite'); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, 'email=' . $email); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_FORBID_REUSE, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); $error = null; @@ -375,8 +391,10 @@ public function RequestSlackInvite($email) { /** * @permission anonymous * - * @param $token - * @param $password + * @param string $token + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException * * @return mixed */ @@ -435,10 +453,10 @@ public function ResetPassword($token, $password) { /** * @permission grants * - * @param $userid - * @param $privilege + * @param int $userid + * @param string $privilege * - * @return mixed + * @return void */ public function RevokePrivilege($userid, $privilege) { if (!CurrentUser::canGrantAllPermissions($privilege)) { @@ -459,6 +477,8 @@ public function RevokePrivilege($userid, $privilege) { $remove = null; + // TODO fix typing + /** @disregard P1006 override */ foreach ($user->privileges->all() as $p) { if ($p->code == $priv->code) { $remove = $p; @@ -466,6 +486,8 @@ public function RevokePrivilege($userid, $privilege) { } if (!is_null($remove)) { + // TODO fix typing + /** @disregard P1006 override */ $user->privileges->remove($remove); $user->save(); } @@ -474,10 +496,10 @@ public function RevokePrivilege($userid, $privilege) { /** * @permission administrator * - * @param $userid - * @param $cash + * @param int $userid + * @param bool $cash * - * @return mixed + * @return void */ public function UpdateCash($userid, $cash) { $user = User::find($userid); @@ -494,10 +516,10 @@ public function UpdateCash($userid, $cash) { /** * @permission administrator|full-profile * - * @param $userid - * @param $email + * @param int $userid + * @param string $email * - * @return mixed + * @return void */ public function UpdateEmail($userid, $email) { $user = $this->GetUser($userid); @@ -514,8 +536,8 @@ public function UpdateEmail($userid, $email) { /** * @permission administrator * - * @param $userid - * @param $date + * @param int $userid + * @param string $date * * @return mixed */ @@ -534,14 +556,18 @@ public function UpdateExpiry($userid, $date) { /** * @permission administrator * - * @param $userid - * @param $membershipid + * @param int $userid + * @param int $membershipid + * + * @throws \app\exceptions\InvalidInputException * * @return mixed */ public function UpdateMembership($userid, $membershipid) { + /** @var User|null */ $user = User::find($userid); + /** @var Membership|null */ $membership = Membership::find($membershipid); if (is_null($user) || is_null($membership)) { @@ -556,9 +582,9 @@ public function UpdateMembership($userid, $membershipid) { /** * @permission administrator|full-profile * - * @param $userid - * @param $fname - * @param $lname + * @param int $userid + * @param string $fname + * @param string $lname * * @return mixed */ @@ -578,8 +604,10 @@ public function UpdateName($userid, $fname, $lname) { /** * @permission administrator|user * - * @param $userid - * @param $subscribe + * @param int $userid + * @param bool $subscribe + * + * @return void */ public function UpdateNewsletter($userid, $subscribe) { $user = $this->GetUser($userid); @@ -596,8 +624,10 @@ public function UpdateNewsletter($userid, $subscribe) { /** * @permission administrator|user * - * @param $userid - * @param $password + * @param int $userid + * @param string $password + * + * @throws \app\exceptions\InvalidPasswordHashException */ public function UpdatePassword($userid, $password) { $user = $this->GetUser($userid); @@ -620,8 +650,8 @@ public function UpdatePassword($userid, $password) { /** * @permission administrator|full-profile * - * @param $userid - * @param $email + * @param int $userid + * @param string $email * * @return void */ @@ -640,8 +670,8 @@ public function UpdatePaymentEmail($userid, $email) { /** * @permission administrator * - * @param $userid - * @param $status + * @param int $userid + * @param string $status * * @return mixed */ @@ -657,17 +687,21 @@ public function UpdateStatus($userid, $status) { case 'y': case 'true': $status = 'y'; + break; case 'pending': case 't': $status = 't'; + break; case 'banned': case 'b': $status = 'b'; + break; default: $status = 'n'; + break; } @@ -679,8 +713,8 @@ public function UpdateStatus($userid, $status) { /** * @permission administrator|full-profile * - * @param $userid - * @param $email + * @param int $userid + * @param string $email * * @return void */ @@ -699,8 +733,10 @@ public function UpdateStripeEmail($userid, $email) { /** * @permission administrator|user * - * @param $userid - * @param $username + * @param int $userid + * @param string $username + * + * @return void */ public function UpdateUsername($userid, $username) { $user = $this->GetUser($userid); @@ -714,6 +750,11 @@ public function UpdateUsername($userid, $username) { $user->save(); } + /** + * AllowedColumns. + * + * @return string[]|null + */ protected function AllowedColumns() { if (CurrentUser::hasAnyPermissions('grants') && !CurrentUser::hasAnyPermissions('administrator')) { return ['id', 'username', 'fname', 'lname', 'email']; diff --git a/packages/backend-php/app/services/WebHookService.php b/packages/backend-php/app/services/WebHookService.php new file mode 100644 index 00000000..440ffcca --- /dev/null +++ b/packages/backend-php/app/services/WebHookService.php @@ -0,0 +1,308 @@ +AddUserIDToFilters($userid, $filters); + + return WebHook::count($filters); + } + + /** + * @permission user + * + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return mixed + */ + public function CreateHook($name, $description, $enabled, $url, $translation, $headers, $method, $eventid) { + $event = (new EventService($this->context))->GetEvent($eventid); + + $codes = []; + foreach ($event->privileges->all() as $priv) { + array_push($codes, $priv->code); + } + + if (!CurrentUser::hasAllPermissions('administrator') && (count($codes) == 0 || !CurrentUser::hasAllPermissions(...$codes))) { + throw new UnauthorizedException('Insufficient privileges to subscribe to event'); + } + + $hook = new WebHook(); + + $hook->name = $name; + $hook->description = $description; + $hook->enabled = $enabled; + $hook->url = $url; + $hook->translation = $translation; + $hook->headers = $headers; + $hook->method = $method; + $hook->event = $event; + $hook->userid = CurrentUser::getIdentity(); + + return $hook->save(); + } + + /** + * @permission administrator|user + * + * @param int $id + * + * @return mixed + */ + public function DeleteHook($id) { + $hook = $this->GetHook($id); + + if (is_null($hook)) { + return; + } + + $hook->delete(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param bool $enabled + * + * @return mixed + */ + public function EnableHook($id, $enabled) { + $hook = $this->GetHook($id); + + if (is_null($hook)) { + return; + } + + $hook->enabled = $enabled; + + $hook->save(); + } + + /** + * @permission webhook|administrator + * + * @return mixed + */ + public function GetAllHooks() { + return WebHook::findAll(); + } + + /** + * @permission user|administrator + * + * @param int $id + * + * @return \app\domain\WebHook|null + */ + public function GetHook($id) { + /** @var \app\domain\WebHook */ + $hook = WebHook::find($id); + + if (is_null($hook)) { + return null; + } + + if (CurrentUser::getIdentity() == $hook->userid || CurrentUser::hasAnyPermissions('administrator')) { + return $hook; + } + + return null; + } + + /** + * @permission webhook|administrator + * + * @param string $domain + * @param string $event + * + * @return mixed + */ + public function GetHooks($domain, $event) { + return WebHook::findByDomainEvent($domain, $event); + } + + /** + * @permission administrator|webhook + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return mixed + */ + public function ListHooks($page, $size, $columns, $order, $filters) { + return WebHook::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $userid + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * + * @return mixed + */ + public function ListUserHooks($userid, $page, $size, $columns, $order, $filters) { + $filters = $this->AddUserIDToFilters($userid, $filters); + + $cols = explode(',', $columns); + + array_push($cols, 'userid'); + + $columns = implode(',', array_unique($cols)); + + return WebHook::page($page, $size, $columns, $order, $filters); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string $privileges + * + * @return mixed + */ + public function PutHookPrivileges($id, $privileges) { + $hook = $this->GetHook($id); + + if (is_null($hook)) { + return; + } + + $privArray = is_string($privileges) ? explode(',', $privileges) : $privileges; + + $privs = Privilege::findByCodes(...$privArray); + + foreach ($hook->privileges->all() as $priv) { + $hook->privileges->remove($priv); + } + + foreach ($privs as $priv) { + if (CurrentUser::hasAnyPermissions('administrator') || CurrentUser::hasAnyPermissions($priv->code)) { + $hook->privileges->add($priv); + } + } + + $hook->save(); + } + + /** + * @permission administrator|user + * + * @param int $id + * @param string $name + * @param string $description + * @param bool $enabled + * @param string $url + * @param string $translation + * @param string $headers + * @param string $method + * @param int $eventid + * + * @return mixed + */ + public function UpdateHook($id, $name, $description, $enabled, $url, $translation, $headers, $method, $eventid) { + $hook = $this->GetHook($id); + + if (is_null($hook)) { + return; + } + + $event = (new EventService($this->context))->GetEvent($eventid); + + $hook->name = $name; + $hook->description = $description; + $hook->enabled = $enabled; + $hook->url = $url; + $hook->translation = $translation; + $hook->headers = $headers; + $hook->method = $method; + $hook->event = $event; + + $hook->save(); + } + + /** + * AddUserIDToFilters. + * + * @param int $userid + * @param Filter|string $filters + * + * @throws \vhs\security\exceptions\UnauthorizedException + * + * @return Filter + */ + private function AddUserIDToFilters($userid, $filters) { + $userService = new UserService(); + $user = $userService->GetUser($userid); + + Domain::coerceFilters($filters); + + if (is_null($user)) { + throw new UnauthorizedException('User not found or you do not have access'); + } + + $userFilter = Filter::Equal('userid', $user->id); + + if (is_null($filters) || $filters == '') { + $filters = $userFilter; + } else { + $filters = Filter::_And($userFilter, $filters); + } + + return $filters; + } +} diff --git a/packages/backend-php/app/utils/AuthCheckResult.php b/packages/backend-php/app/utils/AuthCheckResult.php new file mode 100644 index 00000000..8da2479c --- /dev/null +++ b/packages/backend-php/app/utils/AuthCheckResult.php @@ -0,0 +1,36 @@ + */ + private array $store = []; + + public function __get(string $name): mixed { + return $this->store[$name]; + } + + public function __isset(string $name): bool { + return isset($this->store[$name]); + } + + public function __serialize(): array { + return ['valid' => $this->valid, 'type' => $this->type, 'privileges' => $this->privileges, ...$this->store]; + } + + public function __set(string $name, mixed $value): void { + $this->store[$name] = $value; + } + + public function __unset(string $name): void { + unset($this->store[$name]); + } +} diff --git a/packages/backend-php/app/utils/DTO.php b/packages/backend-php/app/utils/DTO.php new file mode 100644 index 00000000..ba02f15d --- /dev/null +++ b/packages/backend-php/app/utils/DTO.php @@ -0,0 +1,36 @@ + $data + */ +class DTO extends stdClass { + /** + * __construct. + * + * @param array|object $data + * + * @return void + */ + public function __construct($data) { + if ( + (gettype($data) !== 'array' && gettype($data) !== 'object') || + (gettype($data) === 'array' && empty($data)) || + (gettype($data) === 'object' && count(get_object_vars($data)) === 0) + ) { + throw new Exception('Missing DTO construction argument!'); + } + + foreach ($data as $k => $v) { + $this->{$k} = $v; + } + } +} diff --git a/packages/backend-php/app/utils/EnumMapper.php b/packages/backend-php/app/utils/EnumMapper.php new file mode 100644 index 00000000..4eb7ad7a --- /dev/null +++ b/packages/backend-php/app/utils/EnumMapper.php @@ -0,0 +1,23 @@ + $e->name, array_values($cases)); + + if (in_array($needle, $vals)) { + return array_search($needle, $cases); + } + + return null; + } +} diff --git a/packages/backend-php/app/utils/IDTO.php b/packages/backend-php/app/utils/IDTO.php new file mode 100644 index 00000000..3a656852 --- /dev/null +++ b/packages/backend-php/app/utils/IDTO.php @@ -0,0 +1,6 @@ +\n ]/', trim($result)); + + foreach ($matches as $match) { + if (strrpos($match, '\\') !== false) { + $result = str_replace($match, PHP2TS::getBaseContractInterface($match), subject: $result); + } + } + + return $result; + } + + /** + * convertDocComment. + * + * @param string $docComment + * + * @return string + */ + public static function convertDocComment($docComment) { + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($docComment); + + $reconstructed = []; + + if (strlen(trim(string: $docblock->getSummary())) > 0) { + $reconstructed[] = $docblock->getSummary(); + } + + if (strlen(trim(string: $docblock->getDescription())) > 0) { + if (!empty($reconstructed)) { + $reconstructed[] = ''; + } + array_push($reconstructed, ...explode("\n", $docblock->getDescription())); + } + + $lastTag = ''; + + /** @var \phpDocumentor\Reflection\DocBlock\Tags\Param[]|\phpDocumentor\Reflection\DocBlock\Tags\Property[]|\phpDocumentor\Reflection\DocBlock\Tags\TagWithType[] */ + $tags = $docblock->getTags(); + + $longestType = max(array_map(fn ($tag): int => $tag->getName() === 'param' ? strlen(PHP2TS::convertDataType($tag->getType())) + 2 : 0, $tags)); + $longestVarName = max(array_map(fn ($tag): int => $tag->getName() === 'param' ? strlen($tag->getVariableName()) : 0, $tags)); + + foreach ($tags as $tag) { + if ($tag->getName() !== $lastTag) { + if (!empty($reconstructed)) { + $reconstructed[] = ''; + } + $lastTag = $tag->getName(); + } + + $tagRow = [sprintf('@%s', $tag->getName())]; + + switch ($tag->getName()) { + case 'param': + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad(sprintf('{%s}', PHP2TS::convertDataType($tag->getType())), $longestType, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad($tag->getVariableName(), $longestVarName, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + case 'throws': + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad(sprintf('{%s}', PHP2TS::convertDataType($tag->getType())), $longestVarName + 2, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + case 'return': + $tagRow[0] = '@returns'; + /** @disregard P1013 manually checking */ + $tagRow[] = str_pad(sprintf('{%s}', PHP2TS::convertDataType($tag->getType())), $longestVarName + 1, ' '); + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + default: + /** @disregard P1013 manually checking */ + $tagRow[] = $tag->getDescription(); + + break; + } + + $reconstructed[] = implode(' ', $tagRow); + } + + $output = []; + $output[] = ' /**'; + + array_push($output, ...array_map(fn ($row): string => sprintf(' * %s', $row), $reconstructed)); + + $output[] = ' */'; + + return implode("\n", $output); + } + + /** + * generateContractMethodArgs. + * + * @param mixed $contractMethod + * + * @return string + */ + public static function generateContractMethodArgs($contractMethod): string { + $params = $contractMethod->getParameters(); + + return join( + ', ', + array_map( + fn ($param): string => sprintf( + '%s: %s', + $param->getName(), + trim(str_replace('$', '', PHP2TS::getDocCommentParam($contractMethod, $param->getName()))) + ), + $params + ) + ); + } + + /** + * generateContractMethodParams. + * + * @param mixed $contractMethod + * + * @return string + */ + public static function generateContractMethodParams($contractMethod): string { + $params = $contractMethod->getParameters(); + + return join(', ', array_map(fn ($param): string => sprintf('%s', $param->getName()), $params)); + } + + /** + * generateContractMethodReturnType. + * + * @param mixed $contractMethod + * + * @return string + */ + public static function generateContractMethodReturnType($contractMethod): string { + $docComment = $contractMethod->getDocComment(); + + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($docComment); + + $tags = $docblock->getTags(); + + $tag = null; + + foreach ($tags as $tag) { + if ($tag->getName() === 'return') { + /** @disregard P1013 manually checking */ + return PHP2TS::convertDataType($tag->getType()); + } + } + + throw new \Exception( + sprintf('Missing return statement for: %s->%s', $contractMethod->getDeclaringClass()->getName(), $contractMethod->getName()) + ); + } + + /** + * getBaseContractInterface. + * + * @param string $name + * + * @return string + */ + public static function getBaseContractInterface(string $name): string { + return substr($name, strrpos($name, '\\') + 1); + } + + /** + * getDocCommentParam. + * + * @param mixed $contractMethod + * @param string $param + * + * @return mixed + */ + public static function getDocCommentParam($contractMethod, string $param): mixed { + $docComment = $contractMethod->getDocComment(); + + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($docComment); + + $tags = $docblock->getTags(); + + $tag = null; + + foreach ($tags as $tag) { + /** @disregard P1013 manually checking */ + if ($tag->getName() === 'param' && $tag->getVariableName() === $param) { + /** @disregard P1013 manually checking */ + return PHP2TS::convertDataType($tag->getType()); + } + } + + throw new \Exception( + sprintf( + 'Missing @param doc comment for param %s of %s->%s', + $param, + $contractMethod->getDeclaringClass()->getName(), + $contractMethod->getName() + ) + ); + } +} diff --git a/packages/backend-php/composer.json b/packages/backend-php/composer.json new file mode 100644 index 00000000..8b3fc1bf --- /dev/null +++ b/packages/backend-php/composer.json @@ -0,0 +1,25 @@ +{ + "require": { + "aws/aws-sdk-php": "3.342.2", + "nicmart/string-template": "0.1.3", + "league/oauth2-client": "2.8.1", + "php-amqplib/php-amqplib": "3.7.3", + "stripe/stripe-php": "7.128.0", + "league/oauth2-github": "3.1.1", + "league/oauth2-google": "4.0.1" + }, + "require-dev": { + "phpunit/phpunit": "11.5.3", + "friendsofphp/php-cs-fixer": "v3.68.0", + "spatie/typescript-transformer": "^2.4", + "phpdocumentor/shim": "^3.7", + "vimeo/psalm": "^6.5", + "phpstan/phpstan": "^2.1", + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "config": { + "allow-plugins": { + "phpdocumentor/shim": true + } + } +} diff --git a/packages/backend-php/composer.lock b/packages/backend-php/composer.lock new file mode 100644 index 00000000..c8dc3c93 --- /dev/null +++ b/packages/backend-php/composer.lock @@ -0,0 +1,7749 @@ +{ + "_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": "0c3ee196717e12ed32ba8803751716de", + "packages": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.342.2", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "ef66e0fdba9e7f786a7b1e522f847d76d0320e89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ef66e0fdba9e7f786a7b1e522f847d76d0320e89", + "reference": "ef66e0fdba9e7f786a7b1e522f847d76d0320e89", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://github.com/aws/aws-sdk-php/discussions", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.342.2" + }, + "time": "2025-03-07T19:12:43+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "league/oauth2-client", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.5.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" + }, + "time": "2025-02-26T04:37:30+00:00" + }, + { + "name": "league/oauth2-github", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-github.git", + "reference": "84211f62b757f7266fe605a0aa874a32f52c24fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-github/zipball/84211f62b757f7266fe605a0aa874a32f52c24fd", + "reference": "84211f62b757f7266fe605a0aa874a32f52c24fd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/oauth2-client": "^2.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.4", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steven Maguire", + "email": "stevenmaguire@gmail.com", + "homepage": "https://github.com/stevenmaguire" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://github.com/shadowhand" + } + ], + "description": "Github OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "authorisation", + "authorization", + "client", + "github", + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-github/issues", + "source": "https://github.com/thephpleague/oauth2-github/tree/3.1.1" + }, + "time": "2024-09-03T10:42:10+00:00" + }, + { + "name": "league/oauth2-google", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-google.git", + "reference": "1b01ba18ba31b29e88771e3e0979e5c91d4afe76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-google/zipball/1b01ba18ba31b29e88771e3e0979e5c91d4afe76", + "reference": "1b01ba18ba31b29e88771e3e0979e5c91d4afe76", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "eloquent/phony-phpunit": "^6.0 || ^7.1", + "phpunit/phpunit": "^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Woody Gilk", + "email": "hello@shadowhand.com", + "homepage": "https://shadowhand.com" + } + ], + "description": "Google OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "Authentication", + "authorization", + "client", + "google", + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-google/issues", + "source": "https://github.com/thephpleague/oauth2-google/tree/4.0.1" + }, + "time": "2023-03-17T15:20:52+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" + }, + { + "name": "nicmart/string-template", + "version": "v0.1.3", + "source": { + "type": "git", + "url": "https://github.com/nicmart/StringTemplate.git", + "reference": "2a62c240a35a4a20b1be8bd5aa51d4efe93ee4ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nicmart/StringTemplate/zipball/2a62c240a35a4a20b1be8bd5aa51d4efe93ee4ae", + "reference": "2a62c240a35a4a20b1be8bd5aa51d4efe93ee4ae", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "StringTemplate\\": "src/StringTemplate/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolò Martini", + "email": "nicmartnic@gmail.com" + } + ], + "description": "StringTemplate is a very simple string template engine for php. I've written it to have a thing like sprintf, but with named and nested substutions.", + "support": { + "issues": "https://github.com/nicmart/StringTemplate/issues", + "source": "https://github.com/nicmart/StringTemplate/tree/v0.1.3" + }, + "time": "2022-10-25T08:03:55+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "php-amqplib/php-amqplib", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/php-amqplib/php-amqplib.git", + "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/9f50fe69a9f1a19e2cb25596a354d705de36fe59", + "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-sockets": "*", + "php": "^7.2||^8.0", + "phpseclib/phpseclib": "^2.0|^3.0" + }, + "conflict": { + "php": "7.4.0 - 7.4.1" + }, + "replace": { + "videlalvaro/php-amqplib": "self.version" + }, + "require-dev": { + "ext-curl": "*", + "nategood/httpful": "^0.2.20", + "phpunit/phpunit": "^7.5|^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpAmqpLib\\": "PhpAmqpLib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Alvaro Videla", + "role": "Original Maintainer" + }, + { + "name": "Raúl Araya", + "email": "nubeiro@gmail.com", + "role": "Maintainer" + }, + { + "name": "Luke Bakken", + "email": "luke@bakken.io", + "role": "Maintainer" + }, + { + "name": "Ramūnas Dronga", + "email": "github@ramuno.lt", + "role": "Maintainer" + } + ], + "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", + "homepage": "https://github.com/php-amqplib/php-amqplib/", + "keywords": [ + "message", + "queue", + "rabbitmq" + ], + "support": { + "issues": "https://github.com/php-amqplib/php-amqplib/issues", + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.3" + }, + "time": "2025-02-18T20:11:13+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.43", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", + "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2024-12-14T21:12:59+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "stripe/stripe-php", + "version": "v7.128.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "c704949c49b72985c76cc61063aa26fefbd2724e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/c704949c49b72985c76cc61063aa26fefbd2724e", + "reference": "c704949c49b72985c76cc61063aa26fefbd2724e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v7.128.0" + }, + "time": "2022-05-05T17:18:02+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-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.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "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.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + } + ], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-26T16:07:39+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" + }, + { + "name": "amphp/parallel", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "5113111de02796a782f5d90767455e7391cca190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/5113111de02796a782f5d90767455e7391cca190", + "reference": "5113111de02796a782f5d90767455e7391cca190", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-12-21T01:56:09+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "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.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + }, + "time": "2024-04-30T00:40:11+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.68.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", + "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.2", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.1 || ^6.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.2", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.4", + "infection/infection": "^0.29.8", + "justinrainbow/json-schema": "^5.3 || ^6.0", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.22 || ^10.5.40 || ^11.5.2", + "symfony/var-dumper": "^5.4.48 || ^6.4.15 || ^7.2.0", + "symfony/yaml": "^5.4.45 || ^6.4.13 || ^7.2.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.68.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2025-01-13T17:01:01+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-02-12T12:17:51+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" + }, + "time": "2024-09-08T10:13:13+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "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.0-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.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phar-io/composer-distributor", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/phar-io/composer-distributor.git", + "reference": "dd7d936290b2a42b0c64bfe08090b5c597c280c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/composer-distributor/zipball/dd7d936290b2a42b0c64bfe08090b5c597c280c9", + "reference": "dd7d936290b2a42b0c64bfe08090b5c597c280c9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0", + "ext-dom": "*", + "ext-libxml": "*", + "phar-io/filesystem": "^2.0", + "phar-io/gnupg": "^1.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "PharIo\\ComposerDistributor\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Heigl", + "email": "andreas@heigl.org", + "role": "Developer" + }, + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info", + "role": "Developer" + } + ], + "description": "Base Code for a composer plugin that installs PHAR-files", + "homepage": "https://phar.io", + "keywords": [ + "bin", + "binary", + "composer", + "distribute", + "phar", + "phive" + ], + "support": { + "issues": "https://github.com/phar-io/composer-distributor/issues", + "source": "https://github.com/phar-io/composer-distributor/tree/1.0.2" + }, + "funding": [ + { + "url": "https://phar.io", + "type": "other" + } + ], + "time": "2023-05-31T17:05:49+00:00" + }, + { + "name": "phar-io/executor", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/executor.git", + "reference": "5bfb7400224a0c1cf83343660af85c7f5a073473" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/executor/zipball/5bfb7400224a0c1cf83343660af85c7f5a073473", + "reference": "5bfb7400224a0c1cf83343660af85c7f5a073473", + "shasum": "" + }, + "require": { + "phar-io/filesystem": "^2.0", + "php": "^7.2||^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/phar-io/executor/issues", + "source": "https://github.com/phar-io/executor/tree/1.0.1" + }, + "time": "2020-11-30T10:53:57+00:00" + }, + { + "name": "phar-io/filesystem", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/filesystem.git", + "reference": "222e3ea432262a05706b7066697c21257664d9d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/filesystem/zipball/222e3ea432262a05706b7066697c21257664d9d1", + "reference": "222e3ea432262a05706b7066697c21257664d9d1", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/phar-io/filesystem/issues", + "source": "https://github.com/phar-io/filesystem/tree/2.0.1" + }, + "time": "2020-11-30T10:16:22+00:00" + }, + { + "name": "phar-io/gnupg", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/gnupg.git", + "reference": "ed8ab1740ac4e9db99500e7252911f2821357093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/gnupg/zipball/ed8ab1740ac4e9db99500e7252911f2821357093", + "reference": "ed8ab1740ac4e9db99500e7252911f2821357093", + "shasum": "" + }, + "require": { + "phar-io/executor": "^1.0", + "phar-io/filesystem": "^2.0", + "php": "^7.2||^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "description": "Thin GnuPG wrapper class around the gnupg binary, mimicking the pecl/gnupg api", + "support": { + "issues": "https://github.com/phar-io/gnupg/issues", + "source": "https://github.com/phar-io/gnupg/tree/1.0.3" + }, + "time": "2024-08-22T20:45:57+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" + }, + "time": "2024-12-07T09:39:29+00:00" + }, + { + "name": "phpdocumentor/shim", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/shim.git", + "reference": "0eb695503a5ad73eff5722d71a48317275dc2615" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/shim/zipball/0eb695503a5ad73eff5722d71a48317275dc2615", + "reference": "0eb695503a5ad73eff5722d71a48317275dc2615", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "phar-io/composer-distributor": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "phpDocumentor\\Plugin" + }, + "autoload": { + "psr-4": { + "phpDocumentor\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "source": "https://github.com/phpDocumentor/shim/tree/v3.7.1" + }, + "time": "2025-02-15T11:13:17+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-03-24T13:45:00+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.2" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-25T13:26:39+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-27T05:02:59+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "30e319e578a7b5da3543073e30002bf82042f701" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30e319e578a7b5da3543073e30002bf82042f701", + "reference": "30e319e578a7b5da3543073e30002bf82042f701", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.8", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.2", + "sebastian/comparator": "^6.3.0", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.0", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.3" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-01-13T09:36:00+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": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.6", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.6" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-01-01T16:37:48+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T06:57:01+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:54:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-05T09:17:50+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:35:50+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "spatie/array-to-xml", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" + } + ], + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], + "support": { + "source": "https://github.com/spatie/array-to-xml/tree/3.4.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-12-16T12:45:15+00:00" + }, + { + "name": "spatie/typescript-transformer", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/typescript-transformer.git", + "reference": "130c2447e0aa83f8d8d0ff590bc5bc402b17d641" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/typescript-transformer/zipball/130c2447e0aa83f8d8d0ff590bc5bc402b17d641", + "reference": "130c2447e0aa83f8d8d0ff590bc5bc402b17d641", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18|^5.0", + "php": "^8.0", + "phpdocumentor/type-resolver": "^1.6.2", + "symfony/process": "^5.2|^6.0|^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.40", + "larapack/dd": "^1.1", + "myclabs/php-enum": "^1.7", + "pestphp/pest": "^1.22", + "phpstan/extension-installer": "^1.1", + "phpunit/phpunit": "^9.0", + "spatie/data-transfer-object": "^2.0", + "spatie/enum": "^3.0", + "spatie/pest-plugin-snapshots": "^1.1", + "spatie/temporary-directory": "^1.2|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TypeScriptTransformer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Transform your PHP structures to TypeScript types", + "homepage": "https://github.com/spatie/typescript-transformer", + "keywords": [ + "spatie", + "typescript-transformer" + ], + "support": { + "issues": "https://github.com/spatie/typescript-transformer/issues", + "source": "https://github.com/spatie/typescript-transformer/tree/2.4.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-10-04T13:13:08+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/console", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "e51498ea18570c062e7df29d05a7003585b19b88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88", + "reference": "e51498ea18570c062e7df29d05a7003585b19b88", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.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", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.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.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-12T08:11:12+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "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", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.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.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "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.5-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.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.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.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.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.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T19:00:17+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "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.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-20T11:17:29+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "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.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.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.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "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\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "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\\Php81\\": "" + }, + "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.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "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.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T12:21:46+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "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.5-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.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "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.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-24T10:49:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.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.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "vimeo/psalm", + "version": "6.5.0", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "38fc8444edf0cebc9205296ee6e30e906ade783b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/38fc8444edf0cebc9205296ee6e30e906ade783b", + "reference": "38fc8444edf0cebc9205296ee6e30e906ade783b", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parallel": "^2.3", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.1", + "felixfbecker/language-server-protocol": "^1.5.3", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.17 || ~8.2.4 || ~8.3.0 || ~8.4.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^6.0 || ^7.0", + "symfony/filesystem": "^6.0 || ^7.0" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "amphp/phpunit-util": "^3", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "dg/bypass-finals": "^1.5", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.19", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^6.0 || ^7.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalm-review", + "psalter" + ], + "type": "project", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php", + "static analysis" + ], + "support": { + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" + }, + "time": "2025-02-07T20:42:25+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/conf/config.docker.ini.php b/packages/backend-php/conf/config.docker.ini.php similarity index 94% rename from conf/config.docker.ini.php rename to packages/backend-php/conf/config.docker.ini.php index 52cd4209..1853aa3b 100644 --- a/conf/config.docker.ini.php +++ b/packages/backend-php/conf/config.docker.ini.php @@ -45,6 +45,6 @@ /** * Show MySql Errors. - * Not recomended for live site. true/false + * Not recomended for live site. true/false. */ -define('DEBUG', false); +define('DEBUG', defined('NOMOS_DEBUG')); diff --git a/packages/backend-php/conf/config.ini.template.php b/packages/backend-php/conf/config.ini.template.php new file mode 100644 index 00000000..5cc28073 --- /dev/null +++ b/packages/backend-php/conf/config.ini.template.php @@ -0,0 +1,55 @@ + [ + 'item_name' => 'VHS Keyholder Membership', + 'item_number' => 'vhs_membership_keyholder' + ] +]); + +/** + * Show MySql Errors. + * Not recomended for live site. true/false. + */ +define('DEBUG', defined('NOMOS_DEBUG')); diff --git a/conf/nginx.conf b/packages/backend-php/conf/nginx.conf similarity index 100% rename from conf/nginx.conf rename to packages/backend-php/conf/nginx.conf diff --git a/packages/backend-php/conf/php-fpm/00-defaults.conf b/packages/backend-php/conf/php-fpm/00-defaults.conf new file mode 100644 index 00000000..5d1b88c1 --- /dev/null +++ b/packages/backend-php/conf/php-fpm/00-defaults.conf @@ -0,0 +1,11 @@ +[global] +error_log=/proc/self/fd/2 +log_limit=8192 + +[www] +access.log=/proc/self/fd/2 +clear_env=no +catch_workers_output=yes +decorate_workers_output=no + +listen=0.0.0.0:9000 diff --git a/packages/backend-php/conf/php-fpm/99-no-daemonize.conf b/packages/backend-php/conf/php-fpm/99-no-daemonize.conf new file mode 100644 index 00000000..a53c0e5a --- /dev/null +++ b/packages/backend-php/conf/php-fpm/99-no-daemonize.conf @@ -0,0 +1,2 @@ +[global] +daemonize=no diff --git a/packages/backend-php/conf/php/99-no-fastcgi-logging.ini b/packages/backend-php/conf/php/99-no-fastcgi-logging.ini new file mode 100644 index 00000000..7e9000b4 --- /dev/null +++ b/packages/backend-php/conf/php/99-no-fastcgi-logging.ini @@ -0,0 +1 @@ +fastcgi.logging=Off diff --git a/packages/backend-php/conf/php/99-opcache.ini b/packages/backend-php/conf/php/99-opcache.ini new file mode 100644 index 00000000..93149678 --- /dev/null +++ b/packages/backend-php/conf/php/99-opcache.ini @@ -0,0 +1,2 @@ +[opcache] +opcache.enable=1 diff --git a/packages/backend-php/conf/php/99-sessions-dir.ini b/packages/backend-php/conf/php/99-sessions-dir.ini new file mode 100644 index 00000000..0c9a6a53 --- /dev/null +++ b/packages/backend-php/conf/php/99-sessions-dir.ini @@ -0,0 +1 @@ +session.save_path="/sessions" diff --git a/packages/backend-php/docker/docker_compose_run.sh b/packages/backend-php/docker/docker_compose_run.sh new file mode 100755 index 00000000..1c3b3189 --- /dev/null +++ b/packages/backend-php/docker/docker_compose_run.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# shellcheck shell=bash + +if [ ! -d /sessions ]; then + mkdir -p /sessions +fi + +chown nobody:nobody /sessions && chmod 1777 /sessions + +/usr/local/bin/docker_env_config.sh + +(cd /var/www/html/tools && php migrate.php -b -m -t) + +CMD="$*" + +if [ "${CMD:0:1}" = "-" ]; then + CMD="php-fpm83 ${CMD}" +fi + +exec "${CMD}" diff --git a/packages/backend-php/docker/docker_env_config.sh b/packages/backend-php/docker/docker_env_config.sh new file mode 100755 index 00000000..3b5ee133 --- /dev/null +++ b/packages/backend-php/docker/docker_env_config.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +{ + echo " /var/www/html/conf/env.php diff --git a/packages/backend-php/docker/docker_env_config_local.sh b/packages/backend-php/docker/docker_env_config_local.sh new file mode 100755 index 00000000..fa402a55 --- /dev/null +++ b/packages/backend-php/docker/docker_env_config_local.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +echo " /www/conf/env.php + +service php83-fpm start +nginx diff --git a/packages/backend-php/justfile b/packages/backend-php/justfile new file mode 100644 index 00000000..2d2628c3 --- /dev/null +++ b/packages/backend-php/justfile @@ -0,0 +1,73 @@ +set export := true + +help: + @just -l + +format target: + @echo 'Formatting {{target}}…' + @just "format_{{target}}" + +format_all: + #!/usr/bin/env bash + set -eo pipefail + + echo ${FILES:-.} | xargs -n1 | grep -v -E $(find . -type l | grep -vw node_modules | cut -f2- -d/ | xargs | tr ' ' '|') | xargs pnpm exec prettier -w + +format_php: + #!/usr/bin/env bash + set -eo pipefail + + echo "${FILES:-app/ tests/ tools/ vhs/}" | sed 's:packages/backend-php/::g' | xargs vendor/bin/php-cs-fixer fix --config=./.php-cs-fixer.php + +install target: + @echo 'Installing {{target}}…' + @just "install_{{target}}" + +install_composer: + #!/usr/bin/env bash + set -euo pipefail + + if [ ! -f ./tools/composer.phar ]; then + TMPFILE=$(mktemp) + + EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')" + php -r "copy('https://getcomposer.org/installer', '${TMPFILE}');" + ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', '${TMPFILE}');")" + + if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then + echo >&2 'ERROR: Invalid installer checksum' + rm "${TMPFILE}" + exit 1 + fi + + php "${TMPFILE}" --install-dir ./tools --quiet + rm "${TMPFILE}" + else + echo "composer has already been set up!" + fi + + ./tools/composer.sh install ${COMPOSER_INSTALL_OPT:-} + +run_composer: + echo "Running composer" + ./tools/composer.sh install + +setup target: + @echo 'Setting up {{target}}…' + @just "setup_{{target}}" + +setup_vendor: install_composer run_composer + +test target: + @echo 'Testing {{target}}…' + @just "test_{{target}}" + +test_php: + #!/usr/bin/env bash + set -eo pipefail + + vendor/bin/phpunit ${FILES:-app/ tests/ tools/ vhs/} + +update target: + @echo 'Updating {{target}}…' + @just "update_{{target}}" diff --git a/packages/backend-php/package.json b/packages/backend-php/package.json new file mode 100644 index 00000000..6a827ffe --- /dev/null +++ b/packages/backend-php/package.json @@ -0,0 +1,65 @@ +{ + "author": "", + "dependencies": { + "just-install": "^2.0.2", + "wireit": "^0.14.12" + }, + "description": "NOMOS Our Membership Operations Software. In greek mythology, Nomos is the personified spirit of law.", + "devDependencies": { + "@prettier/plugin-php": "^0.22.2", + "@prettier/plugin-xml": "^3.4.1", + "@tyisi/config-eslint": "^4.0.0", + "@tyisi/config-prettier": "^1.0.1", + "@types/node": "^22.13.1", + "bower": "^1.8.14", + "eslint": "^9.15.0", + "prettier": "^3.0.3", + "prettier-plugin-ini": "^1.3.0", + "prettier-plugin-nginx": "^1.0.3", + "prettier-plugin-sh": "^0.14.0", + "prettier-plugin-sql": "^0.18.1", + "prettier-plugin-tailwindcss": "^0.6.11" + }, + "keywords": [], + "license": "ISC", + "main": "index.js", + "name": "@vhs/nomos-backend-php", + "private": true, + "scripts": { + "format:php": "wireit", + "prepare": "wireit", + "test": "wireit", + "test:php": "wireit" + }, + "version": "1.0.0", + "wireit": { + "format:php": { + "command": "just format php" + }, + "prepare": { + "dependencies": [ + "prepare:vendor" + ] + }, + "prepare:vendor": { + "command": "just setup vendor" + }, + "test": { + "dependencies": [ + "test:php" + ] + }, + "test:php": { + "command": "just test php" + } + }, + "files": [ + "app/**/*.php", + "conf/**/*", + "docker/*.sh", + "vhs/**/*.php", + "justfile", + "tools/*", + "composer.*" + ] +} diff --git a/packages/backend-php/php-ts-transformer.php b/packages/backend-php/php-ts-transformer.php new file mode 100644 index 00000000..f7633a65 --- /dev/null +++ b/packages/backend-php/php-ts-transformer.php @@ -0,0 +1,34 @@ +autoDiscoverTypes(__DIR__ . '/app') + // ->autoDiscoverTypes(__DIR__ . '/vhs/Cloneable.php') + // ->autoDiscoverTypes(__DIR__ . '/vhs/Logger.php') + // ->autoDiscoverTypes(__DIR__ . '/vhs/Singleton.php') + // ->autoDiscoverTypes(__DIR__ . '/vhs/SplClassLoader.php') + ->autoDiscoverTypes(__DIR__ . '/vhs/database') + ->autoDiscoverTypes(__DIR__ . '/vhs/domain') + ->autoDiscoverTypes(__DIR__ . '/vhs/loggers') + // ->autoDiscoverTypes(__DIR__ . '/vhs/messaging') + ->autoDiscoverTypes(__DIR__ . '/vhs/migration') + ->autoDiscoverTypes(__DIR__ . '/vhs/monitors') + ->autoDiscoverTypes(__DIR__ . '/vhs/security') + ->autoDiscoverTypes(__DIR__ . '/vhs/services') + // ->autoDiscoverTypes(__DIR__ . '/vhs/vhs.php') + ->autoDiscoverTypes(__DIR__ . '/vhs/web') + // list of transformers + ->transformers([InterfaceTransformer::class, EnumTransformer::class, DtoTransformer::class]) + // file where TypeScript type definitions will be written + ->outputFile(__DIR__ . '/packages/frontend-react/src/types/nomos.d.ts'); + +TypeScriptTransformer::create($config)->transform(); diff --git a/packages/backend-php/phpstan.neon b/packages/backend-php/phpstan.neon new file mode 100644 index 00000000..3420b19d --- /dev/null +++ b/packages/backend-php/phpstan.neon @@ -0,0 +1,23 @@ +parameters: + level: 6 + paths: + - app/ + - tests/ + - tools/ + - vhs/ + + bootstrapFiles: + - conf/config.ini.php + - app/include.php + + scanFiles: + - conf/config.ini.php + - app/include.php + - vhs/vhs.php + + treatPhpDocTypesAsCertain: false + + excludePaths: + - app/utils/converters/PHP2TS.php + - '**/.vscode-server/' + - vendor/ diff --git a/phpunit.xml b/packages/backend-php/phpunit.xml similarity index 100% rename from phpunit.xml rename to packages/backend-php/phpunit.xml diff --git a/packages/backend-php/psalm.xml b/packages/backend-php/psalm.xml new file mode 100644 index 00000000..7c856751 --- /dev/null +++ b/packages/backend-php/psalm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/packages/backend-php/tests/ConstantsTest.php b/packages/backend-php/tests/ConstantsTest.php new file mode 100644 index 00000000..403f9f4a --- /dev/null +++ b/packages/backend-php/tests/ConstantsTest.php @@ -0,0 +1,92 @@ +assertEquals('Y-m-d 00:00:00', Formats::DATE_TIME_ISO_SHORT_MIDNIGHT); + } + + /** + * test_DateTime_DATE_TIME_SIMPLE. + * + * @return void + */ + public function test_DateTime_DATE_TIME_SIMPLE(): void { + $this->assertEquals('Y-m-d H:i:s', Formats::DATE_TIME_ISO_SHORT_FULL); + } + + /** + * test_Errors_E_INVALID_PASSWORD_HASH. + * + * @return void + */ + public function test_Errors_E_INVALID_PASSWORD_HASH(): void { + $this->assertEquals('Invalid password hash', Errors::E_INVALID_PASSWORD_HASH); + } + + /** + * test_StringLiterals_AuthAccessDenied. + * + * @return void + */ + public function test_StringLiterals_AuthAccessDenied(): void { + $this->assertEquals('Access Denied', StringLiterals::AUTH_ACCESS_DENIED); + } + + /** + * test_StringLiterals_AuthAccessGranted. + * + * @return void + */ + public function test_StringLiterals_AuthAccessGranted(): void { + $this->assertEquals('Access Granted', StringLiterals::AUTH_ACCESS_GRANTED); + } + + /** + * test_StringLiterals_HTTP_PREFIX. + * + * @return void + */ + public function test_StringLiterals_HTTP_PREFIX(): void { + $this->assertEquals('http://', StringLiterals::HTTP_PREFIX); + } + + /** + * test_StringLiterals_HTTPS_PREFIX. + * + * @return void + */ + public function test_StringLiterals_HTTPS_PREFIX(): void { + $this->assertEquals('https://', StringLiterals::HTTPS_PREFIX); + } + + protected function setUp(): void { + // Override + } + + /** + * tearDown. + * + * @return void + */ + protected function tearDown(): void { + // Override + } +} diff --git a/tests/DomainTest.php b/packages/backend-php/tests/DomainTest.php similarity index 76% rename from tests/DomainTest.php rename to packages/backend-php/tests/DomainTest.php index b2dd3da2..fec78eb6 100644 --- a/tests/DomainTest.php +++ b/packages/backend-php/tests/DomainTest.php @@ -8,68 +8,36 @@ */ use PHPUnit\Framework\TestCase; -use vhs\database\constraints\Constraint; -use vhs\database\Table; -use vhs\database\types\Type; +use tests\domain\ExampleDomain; +use tests\schema\ExampleSchema; use vhs\database\wheres\Where; -use vhs\domain\Domain; -use vhs\domain\Schema; -use vhs\domain\validations\ValidationFailure; -use vhs\domain\validations\ValidationResults; -class ExampleSchema extends Schema { +/** @typescript */ +class DomainTest extends TestCase { /** - * @return Table + * inMemoryEngine. + * + * @var mixed */ - public static function init() { - $table = new Table('example', null); - - $table->addColumn('id', Type::Int()); - $table->addColumn('testA', Type::String(true)); - $table->addColumn('testB', Type::String(true)); - $table->addColumn('testC', Type::String(true)); - - $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); - - return $table; - } -} - -class ExampleDomain extends Domain { - public static function Define() { - ExampleDomain::Schema(ExampleSchema::Type()); - } - - public function get_magic() { - return 'magic field'; - } - - public function get_testC() { - return $this->internal_testC . 'fail'; - } - - public function set_magic($value) { - $this->testC = $value . 'magic'; - } - - public function set_testC($value) { - $this->internal_testC = $value . 'pass'; - } - - public function validate(ValidationResults &$results) { - if ($this->testA != 'pass') { - $results->add(new ValidationFailure('testA is not equal to pass')); - } - } -} - -class DomainTest extends TestCase { private static $inMemoryEngine; + + /** + * $logger. + * + * @var \vhs\Logger + */ private static $logger; - private static $mySqlEngine; - public function stuff() { + // private static $mySqlEngine; + + /** + * stuff. + * + * @return void + */ + public function stuff(): void { $eg = new ExampleDomain(); + $eg->testA = 'pass'; $eg->testB = 'blimey'; @@ -79,6 +47,7 @@ public function stuff() { unset($eg); + /** @var ExampleDomain */ $eg = ExampleDomain::find(['id' => 1]); $this->assertEquals('blimey', $eg->testB); @@ -106,6 +75,7 @@ public function stuff() { $eg3->testB = 'eg'; $eg3->save(); + /** @var ExampleDomain[] */ $records = ExampleDomain::where(Where::Equal(ExampleSchema::Columns()->testB, 'eg1')); $this->assertEquals(1, sizeof($records)); @@ -127,7 +97,12 @@ public function stuff() { $this->assertEquals('eg', $records[1]->testB); } - public function test_childRelationship() { + /** + * test_childRelationship. + * + * @return void + */ + public function test_childRelationship(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $knight = new \tests\domain\Knight(); @@ -145,6 +120,7 @@ public function test_childRelationship() { unset($knight); + /** @var \tests\domain\Knight */ $knight = \tests\domain\Knight::find($knightid); $this->assertEquals(1, count($knight->rings->all())); @@ -158,12 +134,22 @@ public function test_childRelationship() { //\vhs\database\Database::arbitrary("DROP TABLE example;"); //} - public function test_InMemoryDomainTest() { + /** + * test_InMemoryDomainTest. + * + * @return void + */ + public function test_InMemoryDomainTest(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $this->stuff(); } - public function test_parentRelationship() { + /** + * test_parentRelationship. + * + * @return void + */ + public function test_parentRelationship(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $sword = new \tests\domain\Sword(); @@ -182,7 +168,11 @@ public function test_parentRelationship() { unset($knight); - $knight = \tests\domain\Knight::where(Where::Equal(\tests\domain\Knight::Schema()->Columns()->name, 'Black Knight')); + $knight = \tests\domain\Knight::where( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Where::Equal(\tests\domain\Knight::Schema()->Columns()->name, 'Black Knight') + ); $this->assertEquals(1, count($knight)); @@ -194,7 +184,12 @@ public function test_parentRelationship() { $this->assertEquals('Mighty Sword', $knight->sword->name); } - public function test_satelliteRelationship() { + /** + * test_satelliteRelationship. + * + * @return void + */ + public function test_satelliteRelationship(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); $enchantment = new \tests\domain\Enchantment(); @@ -213,6 +208,7 @@ public function test_satelliteRelationship() { unset($enchantment, $sword); + /** @var \tests\domain\Sword */ $sword = \tests\domain\Sword::find($swordid); $enchants = $sword->enchantments->all(); @@ -225,6 +221,11 @@ public function test_satelliteRelationship() { $enchants[$enchantmentid]->delete(); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new \vhs\loggers\ConsoleLogger(); self::$inMemoryEngine = new \vhs\database\engines\memory\InMemoryEngine(); @@ -248,15 +249,30 @@ public static function setUpBeforeClass(): void { \vhs\database\Database::setEngine(self::$inMemoryEngine); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { //\vhs\database\Database::setEngine(self::$mySqlEngine); //\vhs\database\Database::arbitrary("DROP DATABASE " . DB_DATABASE); } + /** + * setUp. + * + * @return void + */ protected function setUp(): void { } + /** + * tearDown. + * + * @return void + */ protected function tearDown(): void { } } diff --git a/tests/EmailTemplateDomainTest.php b/packages/backend-php/tests/EmailTemplateDomainTest.php similarity index 83% rename from tests/EmailTemplateDomainTest.php rename to packages/backend-php/tests/EmailTemplateDomainTest.php index 0814088e..e41cee49 100644 --- a/tests/EmailTemplateDomainTest.php +++ b/packages/backend-php/tests/EmailTemplateDomainTest.php @@ -20,12 +20,22 @@ class EmailTemplateDomainTest extends TestCase { /** @var Logger */ private static $logger; - private $ids = []; - - public function test_Service() { + /** + * ids. + * + * @ var array + */ + // private $ids = []; + + /** + * test_Service. + * + * @return void + */ + public function test_Service(): void { $service = new EmailService(); - $rows = $service->ListTemplates(1, 1, 'id', 'id', ''); + $rows = array_map(fn ($row): array => get_object_vars($row), array: json_decode(json_encode($service->ListTemplates(1, 1, 'id', 'id', '')))); $this->assertCount(1, $rows); @@ -88,7 +98,7 @@ public function test_Service() { $service->PutTemplate('qwer', 'qwer', 'qwer', 'qwer', 'qwer', 'qwer'); - $rows = $service->ListTemplates(1, 1, 'id', 'id', ''); + $rows = array_map(fn ($row): array => get_object_vars($row), json_decode(json_encode($service->ListTemplates(1, 1, 'id', 'id', '')))); $this->assertCount(1, $rows); @@ -108,18 +118,28 @@ public function test_Service() { $this->assertEquals('qwer', $template->html); } - public function test_Template() { + /** + * test_Template. + * + * @return void + */ + public function test_Template(): void { $generated = EmailTemplate::generate('some_random_name', [ 'a' => 'the value for a', 'other_value' => 'some other value', 'random' => 'random' ]); - $this->assertEquals('the value for a some other value asdf', $generated['subject']); - $this->assertEquals('the value for a some other value qwer', $generated['txt']); - $this->assertEquals('the value for a some other value zxcv', $generated['html']); + $this->assertEquals('the value for a some other value asdf', $generated->subject); + $this->assertEquals('the value for a some other value qwer', $generated->txt); + $this->assertEquals('the value for a some other value zxcv', $generated->html); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new ConsoleLogger(); self::$engine = new InMemoryEngine(); @@ -129,21 +149,38 @@ public static function setUpBeforeClass(): void { Database::setRethrow(true); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { self::$engine->disconnect(); } + /** + * setUp. + * + * @return void + */ public function setUp(): void { $template = new EmailTemplate(); + $template->name = 'This is the most random template, srsly'; $template->code = 'some_random_name'; $template->subject = '{{a}} {{other_value}} asdf'; $template->help = 'some help text to describe whatever this is'; $template->body = '{{a}} {{other_value}} qwer'; $template->html = '{{a}} {{other_value}} zxcv'; + $template->save(); } + /** + * tearDown. + * + * @return void + */ public function tearDown(): void { self::$engine->disconnect(); } diff --git a/tests/KeyDomainTest.php b/packages/backend-php/tests/KeyDomainTest.php similarity index 87% rename from tests/KeyDomainTest.php rename to packages/backend-php/tests/KeyDomainTest.php index 05b21533..d3d9e96c 100644 --- a/tests/KeyDomainTest.php +++ b/packages/backend-php/tests/KeyDomainTest.php @@ -4,6 +4,7 @@ use app\domain\Membership; use app\domain\Privilege; use app\domain\User; +use app\dto\UserActiveEnum; use app\services\AuthService; use PHPUnit\Framework\TestCase; use vhs\database\Database; @@ -20,12 +21,23 @@ class KeyDomainTest extends TestCase { /** @var InMemoryEngine */ private static $engine; + /** @var Logger */ private static $logger; + /** + * ids. + * + * @var array + */ private $ids = []; - public function test_bullshitPhp() { + /** + * test_bullshitPhp. + * + * @return void + */ + public function test_bullshitPhp(): void { $service = new AuthService(); $result = $service->CheckPin('00011234'); @@ -50,9 +62,19 @@ public function test_bullshitPhp() { $this->assertTrue(is_array($obj->privileges), 'privileges must be an array'); } - public function test_Privileges() { + /** + * test_Privileges. + * + * @return void + */ + public function test_Privileges(): void { + /** @var \app\domain\Privilege */ $inherit = Privilege::find($this->ids['inherit']); + + /** @var \app\domain\Privilege */ $membership_privilege = Privilege::find($this->ids['membership_privilege']); + + /** @var \app\domain\Privilege */ $user_privilege = Privilege::find($this->ids['user_privilege']); $service = new AuthService(); @@ -92,6 +114,11 @@ public function test_Privileges() { $this->assertTrue($user_privilegeFound); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new ConsoleLogger(); self::$engine = new InMemoryEngine(); @@ -101,15 +128,27 @@ public static function setUpBeforeClass(): void { Database::setRethrow(true); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { self::$engine->disconnect(); } + /** + * setUp. + * + * @return void + */ public function setUp(): void { $inherit = new Privilege(); + $inherit->name = 'Inherit Privilege'; $inherit->code = 'inherit'; $inherit->enabled = true; + $inherit->save(); $this->ids['inherit'] = $inherit->id; @@ -143,7 +182,7 @@ public function setUp(): void { $user->membership = $membership; $user->username = 'vbnm'; $user->email = 'nomos_tests@vanhack.ca'; - $user->active = 'y'; + $user->active = UserActiveEnum::ACTIVE->value; $user->privileges->add($user_privilege); $user_expiry = new DateTime('today'); $user_expiry->modify('+1 month'); @@ -162,6 +201,11 @@ public function setUp(): void { $this->ids['key'] = $key->id; } + /** + * tearDown. + * + * @return void + */ public function tearDown(): void { self::$engine->disconnect(); } diff --git a/packages/backend-php/tests/LimitTest.php b/packages/backend-php/tests/LimitTest.php new file mode 100644 index 00000000..7e28d2ad --- /dev/null +++ b/packages/backend-php/tests/LimitTest.php @@ -0,0 +1,141 @@ +generate($this->mySqlGenerator); + $this->assertEquals('', $clause); + + /** @var callable $clause */ + $clause = $limit->generate($this->inMemoryGenerator); + + $this->assertEquals('', $clause); + } + + /** + * test_EmptyOffset. + * + * @return void + */ + public function test_EmptyOffset(): void { + $offset = Offset::Offset(null); + + $clause = $offset->generate($this->mySqlGenerator); + $this->assertEquals('', $clause); + + /** @var callable $clause */ + $clause = $offset->generate($this->inMemoryGenerator); + + $this->assertEquals('', $clause); + } + + /** + * test_HasLimit. + * + * @return void + */ + public function test_HasLimit() { + $limit = Limit::Limit(1); + + $clause = $limit->generate($this->mySqlGenerator); + $this->assertEquals(' LIMIT 1 ', $clause); + + /** @var callable $clause */ + $clause = $limit->generate($this->inMemoryGenerator); + + $this->assertEquals('1', $clause); + } + + /** + * test_HasOffset. + * + * @return void + */ + public function test_HasOffset() { + $offset = Offset::Offset(10); + + $clause = $offset->generate($this->mySqlGenerator); + $this->assertEquals(' OFFSET 10 ', $clause); + + /** @var callable $clause */ + $clause = $offset->generate($this->inMemoryGenerator); + + $this->assertEquals('10', $clause); + } + + /** + * setUpBeforeClass. + * + * @return void + */ + public static function setUpBeforeClass(): void { + self::$logger = new ConsoleLogger(); + Database::setLogger(self::$logger); + Database::setEngine(new \vhs\database\engines\memory\InMemoryEngine()); + Database::setRethrow(true); + } + + /** + * tearDownAfterClass. + * + * @return void + */ + public static function tearDownAfterClass(): void { + } + + /** + * setUp. + * + * @return void + */ + public function setUp(): void { + $this->mySqlGenerator = new \vhs\database\engines\mysql\MySqlGenerator(); + $this->inMemoryGenerator = new \vhs\database\engines\memory\InMemoryGenerator(); + } + + /** + * tearDown. + * + * @return void + */ + public function tearDown(): void { + } +} diff --git a/packages/backend-php/tests/ServiceTest.php b/packages/backend-php/tests/ServiceTest.php new file mode 100644 index 00000000..bd084d7b --- /dev/null +++ b/packages/backend-php/tests/ServiceTest.php @@ -0,0 +1,384 @@ +assertEquals('AllPermMethod!', ServiceClient::web_TestService1_AllPermMethod()); + } + + /** + * test_AllPermMethod_authedOnly. + * + * @return void + */ + public function test_AllPermMethod_authedOnly() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_AllPermMethod(); + } + + /** + * test_AllPermMethod_missingPerm2. + * + * @return void + */ + public function test_AllPermMethod_missingPerm2() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm3')); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_AllPermMethod(); + } + + /** + * test_AllPermMethod_missingPerm3. + * + * @return void + */ + public function test_AllPermMethod_missingPerm3() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2')); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_AllPermMethod(); + } + + /** + * test_AnonMethod_asAnon. + * + * @return void + */ + public function test_AnonMethod_asAnon() { + $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('randomPermission')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnonMethod!', ServiceClient::web_TestService1_AnonMethod()); + } + + /** + * test_AnyPermMethod. + * + * @return void + */ + public function test_AnyPermMethod() { + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm2')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm3')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AnyPermMethod!', ServiceClient::web_TestService1_AnyPermMethod()); + } + + /** + * test_AnyPermMethod_authedOnly. + * + * @return void + */ + public function test_AnyPermMethod_authedOnly() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_AnyPermMethod(); + } + + /** + * test_AnyPermMethod_wrongSet. + * + * @return void + */ + public function test_AnyPermMethod_wrongSet() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('asdf', 'zxcv')); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_AnyPermMethod(); + } + + /** + * test_ArgMethod_asAnon. + * + * @return void + */ + public function test_ArgMethod_asAnon() { + // $data = '{ "a": "hello ", "b": "world" }'; + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('ArgMethod: hello world', ServiceClient::web_TestService1_ArgMethod('hello ', 'world')); + } + + /** + * test_AuthMethod_asAnon. + * + * @return void + */ + public function test_AuthMethod_asAnon() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_AuthMethod(); + } + + /** + * test_AuthMethod_asAuth. + * + * @return void + */ + public function test_AuthMethod_asAuth() { + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AuthMethod!', ServiceClient::web_TestService1_AuthMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('randomPermission')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('AuthMethod!', ServiceClient::web_TestService1_AuthMethod()); + } + + /** + * test_EmptyPermMethod_asAnon. + * + * @return void + */ + public function test_EmptyPermMethod_asAnon() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Service contract method requires permission context.'); + + $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_EmptyPermMethod(); + } + + /** + * test_MissingPermMethod_asAnon. + * + * @return void + */ + public function test_MissingPermMethod_asAnon() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Service contract method requires permission context.'); + + $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_MissingPermMethod(); + } + + /** + * test_MultiPermMethod. + * + * @return void + */ + public function test_MultiPermMethod() { + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('MultiPermMethod!', ServiceClient::web_TestService1_MultiPermMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('MultiPermMethod!', ServiceClient::web_TestService1_MultiPermMethod()); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('perm1', 'perm2', 'perm3')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('MultiPermMethod!', ServiceClient::web_TestService1_MultiPermMethod()); + } + + /** + * test_native_ArgMethod_asAnon. + * + * @return void + */ + public function test_native_ArgMethod_asAnon() { + $data = [ + 'a' => 'hello ', + 'b' => 'world' + ]; + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('ArgMethod: hello world', ServiceClient::native_TestService1_ArgMethod('hello ', 'world')); + //ServiceRegistry::get("native")->handle("/services/native/TestService1.svc/ArgMethod", $data)); + } + + /** + * test_NoDocMethod_asAnon. + * + * @return void + */ + public function test_NoDocMethod_asAnon() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Service contract method requires permission context.'); + + $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_NoDocMethod(); + } + + /** + * test_ObjReturnMethod_asAnon. + * + * @return void + */ + public function test_ObjReturnMethod_asAnon() { + $data = '{ "a": "hello " }'; + + $this->assertEquals('{"retA":"hello "}', ServiceRegistry::get('web')->handle('/services/web/TestService1.svc/ObjReturnMethod', $data)); + } + + /** + * test_PermMethod_asAnon. + * + * @return void + */ + public function test_PermMethod_asAnon() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + $this->assertTrue(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_PermMethod(); + } + + /** + * test_PermMethod_asAuth. + * + * @return void + */ + public function test_PermMethod_asAuth() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Access denied'); + + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal()); + + $this->assertFalse(\vhs\security\CurrentUser::getPrincipal()->isAnon()); + + // @phpstan-ignore staticMethod.notFound + ServiceClient::web_TestService1_PermMethod(); + } + + /** + * test_PermMethod_asPerm. + * + * @return void + */ + public function test_PermMethod_asPerm() { + \vhs\security\CurrentUser::setPrincipal(new PermPrincipal('randomPermission')); + + // @phpstan-ignore staticMethod.notFound + $this->assertEquals('PermMethod!', ServiceClient::web_TestService1_PermMethod()); + } + + /** + * setUpBeforeClass. + * + * @return void + */ + public static function setUpBeforeClass(): void { + $logger = new \vhs\loggers\SilentLogger(); + + ServiceRegistry::register($logger, 'web', 'tests\\endpoints\\web', \vhs\BasePath::getBasePath(false)); + ServiceRegistry::register($logger, 'native', 'tests\\endpoints\\native', \vhs\BasePath::getBasePath(false)); + } + + /** + * tearDownAfterClass. + * + * @return void + */ + public static function tearDownAfterClass(): void { + } + + /** + * setUp. + * + * @return void + */ + protected function setUp(): void { + } + + /** + * tearDown. + * + * @return void + */ + protected function tearDown(): void { + \vhs\security\CurrentUser::setPrincipal(new \vhs\security\AnonPrincipal()); + } +} diff --git a/tests/WhereTest.php b/packages/backend-php/tests/WhereTest.php similarity index 84% rename from tests/WhereTest.php rename to packages/backend-php/tests/WhereTest.php index 10c6d243..1d904d42 100644 --- a/tests/WhereTest.php +++ b/packages/backend-php/tests/WhereTest.php @@ -16,6 +16,7 @@ use vhs\Logger; use vhs\loggers\ConsoleLogger; +/** @typescript */ class TestSchema extends Schema { /** * @return Table @@ -30,14 +31,31 @@ public static function init() { } } +/** @typescript */ class WhereTest extends TestCase { /** @var Logger */ private static $logger; + + /** + * inMemoryGenerator. + * + * @var mixed + */ private $inMemoryGenerator; + /** + * mySqlGenerator. + * + * @var mixed + */ private $mySqlGenerator; - public function test_And() { + /** + * test_And. + * + * @return void + */ + public function test_And(): void { $where = Where::_And(Where::Equal(TestSchema::Column('test1'), 'test2'), Where::Equal(TestSchema::Column('test3'), 'test4')); $clause = $where->generate($this->mySqlGenerator); @@ -61,7 +79,12 @@ public function test_And() { $this->assertFalse($clause($data2)); } - public function test_AndOr() { + /** + * test_AndOr. + * + * @return void + */ + public function test_AndOr(): void { $where = Where::_And( Where::Equal(TestSchema::Column('test1'), 'test2'), Where::_Or(Where::Equal(TestSchema::Column('test3'), 'test4'), Where::Equal(TestSchema::Column('test5'), 'test6')) @@ -111,7 +134,12 @@ public function test_AndOr() { $this->assertFalse($clause($data5)); } - public function test_Equal() { + /** + * test_Equal. + * + * @return void + */ + public function test_Equal(): void { $where = Where::Equal(TestSchema::Column('test1'), 'test2'); $clause = $where->generate($this->mySqlGenerator); @@ -128,7 +156,12 @@ public function test_Equal() { $this->assertFalse($clause($data2)); } - public function test_Greater() { + /** + * test_Greater. + * + * @return void + */ + public function test_Greater(): void { $where = Where::Greater(TestSchema::Column('test1'), 1); $clause = $where->generate($this->mySqlGenerator); @@ -145,7 +178,12 @@ public function test_Greater() { $this->assertTrue($clause($data2)); } - public function test_GreaterEqual() { + /** + * test_GreaterEqual. + * + * @return void + */ + public function test_GreaterEqual(): void { $where = Where::GreaterEqual(TestSchema::Column('test1'), 1); $clause = $where->generate($this->mySqlGenerator); @@ -164,7 +202,12 @@ public function test_GreaterEqual() { $this->assertTrue($clause($data3)); } - public function test_In() { + /** + * test_In. + * + * @return void + */ + public function test_In(): void { $where = Where::In(TestSchema::Column('test1'), ['a', 'b', 'c']); $clause = $where->generate($this->mySqlGenerator); @@ -185,7 +228,12 @@ public function test_In() { $this->assertFalse($clause($data4)); } - public function test_Lesser() { + /** + * test_Lesser. + * + * @return void + */ + public function test_Lesser(): void { $where = Where::Lesser(TestSchema::Column('test1'), 2); $clause = $where->generate($this->mySqlGenerator); @@ -202,7 +250,12 @@ public function test_Lesser() { $this->assertFalse($clause($data2)); } - public function test_LesserEqual() { + /** + * test_LesserEqual. + * + * @return void + */ + public function test_LesserEqual(): void { $where = Where::LesserEqual(TestSchema::Column('test1'), 2); $clause = $where->generate($this->mySqlGenerator); @@ -221,7 +274,12 @@ public function test_LesserEqual() { $this->assertFalse($clause($data3)); } - public function test_NotEqual() { + /** + * test_NotEqual. + * + * @return void + */ + public function test_NotEqual(): void { $where = Where::NotEqual(TestSchema::Column('test1'), 'test2'); $clause = $where->generate($this->mySqlGenerator); @@ -238,7 +296,12 @@ public function test_NotEqual() { $this->assertTrue($clause($data2)); } - public function test_NotIn() { + /** + * test_NotIn. + * + * @return void + */ + public function test_NotIn(): void { $where = Where::NotIn(TestSchema::Column('test1'), ['a', 'b', 'c']); $clause = $where->generate($this->mySqlGenerator); @@ -259,7 +322,12 @@ public function test_NotIn() { $this->assertTrue($clause($data4)); } - public function test_NotNull() { + /** + * test_NotNull. + * + * @return void + */ + public function test_NotNull(): void { $where = Where::NotNull(TestSchema::Column('test1')); $clause = $where->generate($this->mySqlGenerator); @@ -276,7 +344,12 @@ public function test_NotNull() { $this->assertTrue($clause($data2)); } - public function test_Null() { + /** + * test_Null. + * + * @return void + */ + public function test_Null(): void { $where = Where::Null(TestSchema::Column('test1')); $clause = $where->generate($this->mySqlGenerator); @@ -293,7 +366,12 @@ public function test_Null() { $this->assertFalse($clause($data2), "failed by matching 'test3' as null"); } - public function test_Or() { + /** + * test_Or. + * + * @return void + */ + public function test_Or(): void { $where = Where::_Or(Where::Equal(TestSchema::Column('test1'), 'test2'), Where::Equal(TestSchema::Column('test3'), 'test4')); $clause = $where->generate($this->mySqlGenerator); @@ -323,6 +401,11 @@ public function test_Or() { $this->assertFalse($clause($data3)); } + /** + * setUpBeforeClass. + * + * @return void + */ public static function setUpBeforeClass(): void { self::$logger = new ConsoleLogger(); Database::setLogger(self::$logger); @@ -330,14 +413,29 @@ public static function setUpBeforeClass(): void { Database::setRethrow(true); } + /** + * tearDownAfterClass. + * + * @return void + */ public static function tearDownAfterClass(): void { } + /** + * setUp. + * + * @return void + */ public function setUp(): void { $this->mySqlGenerator = new \vhs\database\engines\mysql\MySqlGenerator(); $this->inMemoryGenerator = new \vhs\database\engines\memory\InMemoryGenerator(); } + /** + * tearDown. + * + * @return void + */ public function tearDown(): void { } } diff --git a/packages/backend-php/tests/autoload.php b/packages/backend-php/tests/autoload.php new file mode 100644 index 00000000..52aa00f6 --- /dev/null +++ b/packages/backend-php/tests/autoload.php @@ -0,0 +1,16 @@ +add(new \vhs\SplClassLoaderItem('tests', \vhs\BasePath::getBasePath(false))); +\vhs\SplClassLoader::getInstance()->add(new \vhs\SplClassLoaderItem('app', \vhs\BasePath::getBasePath(false))); diff --git a/tests/contracts/ITestService1.php b/packages/backend-php/tests/contracts/ITestService1.php similarity index 88% rename from tests/contracts/ITestService1.php rename to packages/backend-php/tests/contracts/ITestService1.php index a1d39adc..88f1efd9 100644 --- a/tests/contracts/ITestService1.php +++ b/packages/backend-php/tests/contracts/ITestService1.php @@ -38,8 +38,8 @@ public function AnyPermMethod(); /** * @permission anonymous * - * @param $a - * @param $b + * @param mixed $a + * @param mixed $b * * @return mixed */ @@ -72,12 +72,17 @@ public function MissingPermMethod(); */ public function MultiPermMethod(); - public function NoDocMethod(); + /** + * NoDocMethod. + * + * @return string + */ + public function NoDocMethod(): string; /** * @permission anonymous * - * @param $a + * @param mixed $a * * @return mixed */ diff --git a/packages/backend-php/tests/domain/Enchantment.php b/packages/backend-php/tests/domain/Enchantment.php new file mode 100644 index 00000000..589cd647 --- /dev/null +++ b/packages/backend-php/tests/domain/Enchantment.php @@ -0,0 +1,46 @@ + + * + * @typescript + */ +class Enchantment extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Enchantment::Schema(EnchantmentSchema::Type()); + + //NOTE don't setup the same relationships on the child of a previously defined parent, this will cause a hydrate loop. + //EnchantmentDomain::Relationship("swords", Sword::Type(), SwordEnchantmentsSchema::getInstance()); + } + + /** + * @param ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/tests/domain/ExampleDomain.php b/packages/backend-php/tests/domain/ExampleDomain.php new file mode 100644 index 00000000..cc1272e6 --- /dev/null +++ b/packages/backend-php/tests/domain/ExampleDomain.php @@ -0,0 +1,84 @@ + + * + * @typescript + */ +class ExampleDomain extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + ExampleDomain::Schema(ExampleSchema::Type()); + } + + /** + * get_magic. + * + * @return string + */ + public function get_magic(): string { + return 'magic field'; + } + + /** + * get_testC. + * + * @return string + */ + public function get_testC(): string { + return $this->internal_testC . 'fail'; + } + + /** + * set_magic. + * + * @param mixed $value + * + * @return void + */ + public function set_magic($value) { + $this->testC = $value . 'magic'; + } + + /** + * set_testC. + * + * @param mixed $value + * + * @return void + */ + public function set_testC($value) { + $this->internal_testC = $value . 'pass'; + } + + /** + * validate. + * + * @param \vhs\domain\validations\ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + if ($this->testA != 'pass') { + $results->add(new ValidationFailure('testA is not equal to pass')); + } + } +} diff --git a/packages/backend-php/tests/domain/Knight.php b/packages/backend-php/tests/domain/Knight.php new file mode 100644 index 00000000..b3a8b905 --- /dev/null +++ b/packages/backend-php/tests/domain/Knight.php @@ -0,0 +1,48 @@ + + * + * @typescript + */ +class Knight extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Knight::Schema(KnightSchema::Type()); + Knight::Relationship('sword', Sword::Type()); //parent relationship aka Many to One + Knight::Relationship('rings', Ring::Type()); //child relationship aka One to Many + } + + /** + * @param ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/tests/domain/Ring.php b/packages/backend-php/tests/domain/Ring.php new file mode 100644 index 00000000..f335c6d2 --- /dev/null +++ b/packages/backend-php/tests/domain/Ring.php @@ -0,0 +1,48 @@ + + * + * @typescript + */ +class Ring extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Ring::Schema(RingSchema::Type()); + + Ring::Relationship('enchantment', Enchantment::Type()); //parent relationship Many to One + } + + /** + * @param ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/tests/domain/Sword.php b/packages/backend-php/tests/domain/Sword.php new file mode 100644 index 00000000..103ef49e --- /dev/null +++ b/packages/backend-php/tests/domain/Sword.php @@ -0,0 +1,48 @@ + + * + * @typescript + */ +class Sword extends Domain { + /** + * Define. + * + * @return void + */ + public static function Define(): void { + Sword::Schema(SwordSchema::Type()); + + //NOTE don't setup the same relationships on the child of a previously defined parent, this will cause a hydrate loop. + Sword::Relationship('enchantments', Enchantment::Type(), SwordEnchantmentsSchema::Type()); //satellite relationship aka Many to Many + } + + /** + * @param ValidationResults $results + * + * @return void + */ + public function validate(ValidationResults &$results) { + // TODO: Implement validate() method. + } +} diff --git a/packages/backend-php/tests/endpoints/native/TestService1.svc.php b/packages/backend-php/tests/endpoints/native/TestService1.svc.php new file mode 100644 index 00000000..9c294dff --- /dev/null +++ b/packages/backend-php/tests/endpoints/native/TestService1.svc.php @@ -0,0 +1,21 @@ +addColumn('name', Type::String(false, 'Mystery Enchament', 50)); $table->addColumn('bonus', Type::Float(false, 1.1)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); return $table; diff --git a/packages/backend-php/tests/schema/ExampleSchema.php b/packages/backend-php/tests/schema/ExampleSchema.php new file mode 100644 index 00000000..c0b436b1 --- /dev/null +++ b/packages/backend-php/tests/schema/ExampleSchema.php @@ -0,0 +1,33 @@ +addColumn('id', Type::Int()); + $table->addColumn('testA', Type::String(true)); + $table->addColumn('testB', Type::String(true)); + $table->addColumn('testC', Type::String(true)); + + // TODO fix proper typing + // @phpstan-ignore property.notFound + $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); + + return $table; + } +} diff --git a/packages/backend-php/tests/schema/KnightSchema.php b/packages/backend-php/tests/schema/KnightSchema.php new file mode 100644 index 00000000..e99b7c67 --- /dev/null +++ b/packages/backend-php/tests/schema/KnightSchema.php @@ -0,0 +1,52 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('swordid', Type::Int(false, 0)); + $table->addColumn('name', Type::String(false, 'Mystery Knight', 50)); + $table->addColumn('birthdate', Type::DateTime()); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->swordid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + SwordSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + SwordSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/packages/backend-php/tests/schema/RingSchema.php b/packages/backend-php/tests/schema/RingSchema.php new file mode 100644 index 00000000..ab761f62 --- /dev/null +++ b/packages/backend-php/tests/schema/RingSchema.php @@ -0,0 +1,58 @@ +addColumn('id', Type::Int(false, 0)); + $table->addColumn('name', Type::String(false, 'Mystery Ring', 50)); + $table->addColumn('knightid', Type::Int(false, 0)); + $table->addColumn('enchantmentid', Type::Int(false, 0)); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->id), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->enchantmentid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EnchantmentSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EnchantmentSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->knightid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + KnightSchema::Table(), + KnightSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/packages/backend-php/tests/schema/SwordEnchantmentsSchema.php b/packages/backend-php/tests/schema/SwordEnchantmentsSchema.php new file mode 100644 index 00000000..1525864f --- /dev/null +++ b/packages/backend-php/tests/schema/SwordEnchantmentsSchema.php @@ -0,0 +1,61 @@ +addColumn('swordid', Type::Int(false, 0)); + $table->addColumn('enchantmentid', Type::Int(false, 0)); + + $table->setConstraints( + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->swordid), + // TODO implement proper typing + // @phpstan-ignore property.notFound + Constraint::PrimaryKey($table->columns->enchantmentid), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->swordid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + SwordSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + SwordSchema::Columns()->id + ), + Constraint::ForeignKey( + // TODO implement proper typing + // @phpstan-ignore property.notFound + $table->columns->enchantmentid, + // TODO implement proper typing + // @phpstan-ignore argument.byRef + EnchantmentSchema::Table(), + // TODO implement proper typing + // @phpstan-ignore property.notFound + EnchantmentSchema::Columns()->id + ) + ); + + return $table; + } +} diff --git a/tests/schema/SwordSchema.php b/packages/backend-php/tests/schema/SwordSchema.php similarity index 86% rename from tests/schema/SwordSchema.php rename to packages/backend-php/tests/schema/SwordSchema.php index 8a0575bd..ff1aa808 100644 --- a/tests/schema/SwordSchema.php +++ b/packages/backend-php/tests/schema/SwordSchema.php @@ -14,6 +14,7 @@ use vhs\database\types\Type; use vhs\domain\Schema; +/** @typescript */ class SwordSchema extends Schema { /** * @return Table @@ -25,6 +26,8 @@ public static function init() { $table->addColumn('name', Type::String(false, 'Mystery Sword', 50)); $table->addColumn('damage', Type::Int(false, 5)); + // TODO implement proper typing + // @phpstan-ignore property.notFound $table->setConstraints(Constraint::PrimaryKey($table->columns->id)); return $table; diff --git a/packages/backend-php/tests/security/PermPrincipal.php b/packages/backend-php/tests/security/PermPrincipal.php new file mode 100644 index 00000000..bc4738cc --- /dev/null +++ b/packages/backend-php/tests/security/PermPrincipal.php @@ -0,0 +1,86 @@ +perms = $perms; + } + + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAllPermissions(...$permission) { + // TODO: Implement canGrantAllPermissions() method. + return false; + } + + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function canGrantAnyPermissions(...$permission) { + // TODO: Implement canGrantAnyPermissions() method. + return false; + } + + public function getIdentity() { + return null; + } + + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAllPermissions(...$permission) { + return count(array_diff($permission, $this->perms)) == 0; + } + + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + public function hasAnyPermissions(...$permission) { + return count(array_intersect($permission, $this->perms)) > 0; + } + + public function isAnon() { + return false; + } + + public function __toString() { + return 'perm'; + } +} diff --git a/tests/services/TestService.php b/packages/backend-php/tests/services/TestService.php similarity index 90% rename from tests/services/TestService.php rename to packages/backend-php/tests/services/TestService.php index 6c70af94..db4f3415 100644 --- a/tests/services/TestService.php +++ b/packages/backend-php/tests/services/TestService.php @@ -12,6 +12,7 @@ use tests\contracts\ITestService1; use vhs\services\Service; +/** @typescript */ class TestService extends Service implements ITestService1 { /** * @permission perm1 @@ -45,8 +46,8 @@ public function AnyPermMethod() { /** * @permission anonymous * - * @param $a - * @param $b + * @param mixed $a + * @param mixed $b * * @return mixed */ @@ -89,14 +90,19 @@ public function MultiPermMethod() { return 'MultiPermMethod!'; } - public function NoDocMethod() { + /** + * NoDocMethod. + * + * @return string + */ + public function NoDocMethod(): string { return 'NoDocMethod!'; } /** * @permission anonymous * - * @param $a + * @param mixed $a * * @return mixed */ diff --git a/tools/composer.sh b/packages/backend-php/tools/composer.sh similarity index 100% rename from tools/composer.sh rename to packages/backend-php/tools/composer.sh diff --git a/packages/backend-php/tools/generate-dto-domain-classes.php b/packages/backend-php/tools/generate-dto-domain-classes.php new file mode 100755 index 00000000..357d7b3b --- /dev/null +++ b/packages/backend-php/tools/generate-dto-domain-classes.php @@ -0,0 +1,148 @@ +#!/usr/bin/env php +type))); + + $field = [ + 'name' => $column->name, + 'type' => $baseType + ]; + + switch ($field['type']) { + case 'datetime': + case 'text': + $field['type'] = 'string'; + + break; + case 'enum': + $enumName = sprintf('%s%sEnum', $className, ucfirst($column->name)); + + $field['enum'] = [ + 'name' => $enumName, + 'values' => $column->type->values + ]; + + break; + default: + break; + } + + return $field; +} + +/** + * generateDTOFile. + * + * @param string $dtoFile + * @param string[] $content + */ +function generateDTOFile(string $dtoFile, array $content): void { + printf("Writing %s...\n", $dtoFile); + $output = []; + + $output[] = 'Table()->columns->all() as $column) { + $domains[$className][] = generateFieldDefinition($className, $column); + } + } +} + +foreach ($domains as $className => $fields) { + printf("Generating %s...\n", $className); + + $dtoFile = sprintf('app/dto/%s.php', $className); + + $classContent = []; + + $classContent[] = sprintf('class %s {', $className); + + foreach ($fields as $field) { + if ($field['type'] === 'enum') { + // @phpstan-ignore offsetAccess.notFound + $enumFile = sprintf('app/dto/%s.php', $field['enum']['name']); + + $enumContent = []; + + // @phpstan-ignore offsetAccess.notFound + $enumContent[] = sprintf('enum %s {', $field['enum']['name']); + + // @phpstan-ignore offsetAccess.notFound + foreach ($field['enum']['values'] as $value) { + $enumContent[] = sprintf(' case %s;', $value); + } + + $enumContent[] = '}'; + + generateDTOFile($enumFile, $enumContent); + + // @phpstan-ignore offsetAccess.notFound + $classContent[] = sprintf(' public %s $%s;', $field['enum']['name'], $field['name']); + } elseif ($field['type'] !== 'enum') { + $classContent[] = sprintf(' public %s $%s;', $field['type'], $field['name']); + } + + $classContent[] = ''; + } + + $classContent[] = sprintf(" public function __construct(\app\domain\%s $%s) {", $className, lcfirst($className)); + + foreach ($fields as $field) { + if ($field['type'] === 'enum') { + $classContent[] = sprintf( + ' $this->%s = %s::tryFrom($%s->%s);', + $field['name'], + // @phpstan-ignore offsetAccess.notFound + $field['enum']['name'], + lcfirst($className), + $field['name'] + ); + } else { + $classContent[] = sprintf(' $this->%s = $%s->%s;', $field['name'], lcfirst($className), $field['name']); + } + } + $classContent[] = sprintf(' }'); + + $classContent[] = '}'; + $classContent[] = ''; + + generateDTOFile($dtoFile, $classContent); +} diff --git a/packages/backend-php/tools/generate-dto-schema-classes.php b/packages/backend-php/tools/generate-dto-schema-classes.php new file mode 100644 index 00000000..16f36069 --- /dev/null +++ b/packages/backend-php/tools/generate-dto-schema-classes.php @@ -0,0 +1,72 @@ +type))); + + $field = [ + 'name' => $column->name, + 'type' => $baseType + ]; + + switch ($field['type']) { + case 'datetime': + case 'text': + $field['type'] = 'string'; + + break; + case 'enum': + $enumName = sprintf('%s%sEnum', $className, ucfirst($column->name)); + + $field['enum'] = [ + 'name' => $enumName, + /** + * @disregard P1014 + * + * @phpstan-ignore property.notFound + */ + 'values' => $column->type->values + ]; + + break; + default: + break; + } + + return $field; +} + +foreach (glob('app/schema/*.php') as $filename) { + require_once $filename; +} + +$schemas = []; + +foreach (get_declared_classes() as $class) { + if (is_subclass_of($class, '\\vhs\\domain\\Schema')) { + $schemaName = str_replace('app\\schema\\', '', $class); + $schemas[$schemaName] = $class; + + print_r($class::Table()->columns->all()); + /** @var \vhs\database\Column $column */ + foreach ($class::Table()->columns->all() as $column) { + $schemas[$schemaName][] = generateFieldDefinition($schemaName, $column); + } + } +} + +print_r($schemas); diff --git a/packages/backend-php/tools/generate-ts-contract-implementation.php b/packages/backend-php/tools/generate-ts-contract-implementation.php new file mode 100755 index 00000000..7c465f5c --- /dev/null +++ b/packages/backend-php/tools/generate-ts-contract-implementation.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +getInterfaces() as $interface) { + if (array_key_exists('vhs\\services\\IContract', $interface->getInterfaces())) { + $contract = $interface; + } + } + + $baseInterface = PHP2TS::getBaseContractInterface($contract->getName()); + $baseService = substr($baseInterface, 1); + + printf("export default class %s implements %s {\n", $baseService, $baseInterface); + + $contractMethods = $contract->getMethods(); + + foreach ($contractMethods as $k => $contractMethod) { + if ($k > 0) { + echo "\n"; + } + + printf( + "%s\n async %s(%s): BackendResult<%s> {\n return await backendCall('/services/v2/%s.svc/%s', { %s })\n }\n", + PHP2TS::convertDocComment($contractMethod->getDocComment()), + $contractMethod->getName(), + PHP2TS::generateContractMethodArgs($contractMethod), + PHP2TS::generateContractMethodReturnType($contractMethod), + $baseService, + $contractMethod->getName(), + PHP2TS::generateContractMethodParams($contractMethod) + ); + } + + printf("}\n"); +} diff --git a/packages/backend-php/tools/generate-ts-contract-interface.php b/packages/backend-php/tools/generate-ts-contract-interface.php new file mode 100755 index 00000000..e339d74b --- /dev/null +++ b/packages/backend-php/tools/generate-ts-contract-interface.php @@ -0,0 +1,53 @@ +#!/usr/bin/env php +getInterfaces() as $interface) { + if (array_key_exists('vhs\\services\\IContract', $interface->getInterfaces())) { + $contract = $interface; + } + } + + $baseInterface = PHP2TS::getBaseContractInterface($contract->getName()); + + printf("export interface %s {\n", $baseInterface); + + $contractMethods = $contract->getMethods(); + + foreach ($contractMethods as $k => $contractMethod) { + if ($k > 0) { + echo "\n"; + } + + printf( + "%s\n %s: (%s) => BackendResult<%s>\n", + PHP2TS::convertDocComment($contractMethod->getDocComment()), + $contractMethod->getName(), + PHP2TS::generateContractMethodArgs($contractMethod), + PHP2TS::generateContractMethodReturnType($contractMethod) + ); + } + + printf("}\n"); +} diff --git a/tools/migrate.php b/packages/backend-php/tools/migrate.php similarity index 86% rename from tools/migrate.php rename to packages/backend-php/tools/migrate.php index f3c41aac..89d8c11d 100644 --- a/tools/migrate.php +++ b/packages/backend-php/tools/migrate.php @@ -4,11 +4,11 @@ use vhs\migration\Backup; use vhs\migration\Migrator; -require_once dirname(__FILE__) . '/../vhs/vhs.php'; +require_once __DIR__ . '/../vhs/vhs.php'; define('_VALID_PHP', true); -require_once '../conf/config.ini.php'; +require_once __DIR__ . '/../conf/config.ini.php'; $target_version = null; $do_migrate = null; @@ -44,7 +44,7 @@ } if ($do_migrate) { $migrator = new Migrator(DB_SERVER, DB_USER, DB_PASS, DB_DATABASE, new ConsoleLogger()); - if ($migrator->migrate($target_version, '../migrations')) { + if ($migrator->migrate($target_version, __DIR__ . '/../migrations')) { print "Migration succeeded\n"; } else { print "Migration failed\n"; diff --git a/packages/backend-php/tools/sync-dto-classes.sh b/packages/backend-php/tools/sync-dto-classes.sh new file mode 100755 index 00000000..caebd92e --- /dev/null +++ b/packages/backend-php/tools/sync-dto-classes.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +cd "$(dirname "$(realpath "$0")")/.." || exit 255 + +find app/domain/ -type f -name '*.php' -print0 | xargs -0 -I% basename % .php | sort | while read -r DOMAIN_CLASS_NAME; do + DTO_FILE="app/dto/${DOMAIN_CLASS_NAME}.php" + + cat << EOF > "${DTO_FILE}" + $array + * + * @return array + */ private function __arrayCopy(array $array) { $result = []; + foreach ($array as $key => $val) { if (is_array($val)) { $result[$key] = $this->__arrayCopy($val); @@ -32,7 +38,13 @@ private function __arrayCopy(array $array) { return $result; } - public function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + // @phpstan-ignore foreach.nonIterable foreach ($this as $key => $val) { if (is_object($val)) { $this->{$key} = clone $val; diff --git a/packages/backend-php/vhs/Logger.php b/packages/backend-php/vhs/Logger.php new file mode 100644 index 00000000..9b5a6339 --- /dev/null +++ b/packages/backend-php/vhs/Logger.php @@ -0,0 +1,47 @@ +log(sprintf('%s:%s - %s: %s', str_replace(BasePath::getBasePath(withSlash: true), '', $file), $line, $caller, $msg)); + } + } + + /** + * __toString. + * + * @return string + */ + public function __toString() { + return get_called_class(); + } +} diff --git a/packages/backend-php/vhs/Loggington.php b/packages/backend-php/vhs/Loggington.php new file mode 100644 index 00000000..e8e59f34 --- /dev/null +++ b/packages/backend-php/vhs/Loggington.php @@ -0,0 +1,19 @@ +logger = new SilentLogger(); + } + + public function setLogger(Logger &$logger): void { + $this->logger = &$logger; + } +} diff --git a/packages/backend-php/vhs/Singleton.php b/packages/backend-php/vhs/Singleton.php new file mode 100644 index 00000000..36129d90 --- /dev/null +++ b/packages/backend-php/vhs/Singleton.php @@ -0,0 +1,46 @@ +namespace = $namespace; $this->includePath = $includePath; @@ -14,13 +41,30 @@ public function __construct($namespace = null, $includePath = null, $fileExtensi } } +/** @typescript */ class SplClassLoader { + /** + * namespace items. + * + * @var array + */ private $_items = []; + + /** + * namespace separator. + * + * @var string + */ private $_namespaceSeparator = '\\'; protected function __construct() { } + /** + * getInstance. + * + * @return \vhs\SplClassLoader + */ final public static function getInstance() { static $aoInstance = []; @@ -31,6 +75,13 @@ final public static function getInstance() { return $aoInstance['SplClassLoader']; } + /** + * add a new item. + * + * @param \vhs\SplClassLoaderItem $item + * + * @return void + */ public function add(SplClassLoaderItem $item) { $this->_items[$item->namespace] = $item; $this->register(); @@ -88,18 +139,26 @@ public function loadClass($className) { $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $item->fileExtension; - //print ($item->includePath !== null ? $item->includePath . DIRECTORY_SEPARATOR : '') . $fileName; require ($item->includePath !== null ? $item->includePath . DIRECTORY_SEPARATOR : '') . $fileName; } } /** * Installs this class loader on the SPL autoload stack. + * + * @return void */ public function register() { spl_autoload_register([$this, 'loadClass']); } + /** + * remove an item. + * + * @param string $namespace + * + * @return void + */ public function remove($namespace) { if (array_key_exists($namespace, $this->_items)) { unset($this->_items[$namespace]); @@ -110,6 +169,8 @@ public function remove($namespace) { * Sets the namespace separator used by classes in the namespace of this class loader. * * @param string $sep the separator to use + * + * @return void */ public function setNamespaceSeparator($sep) { $this->_namespaceSeparator = $sep; @@ -117,11 +178,18 @@ public function setNamespaceSeparator($sep) { /** * Uninstalls this class loader from the SPL autoloader stack. + * + * @return void */ public function unregister() { spl_autoload_unregister([$this, 'loadClass']); } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/packages/backend-php/vhs/database/Column.php b/packages/backend-php/vhs/database/Column.php new file mode 100644 index 00000000..2735b671 --- /dev/null +++ b/packages/backend-php/vhs/database/Column.php @@ -0,0 +1,175 @@ +table = $table; + $this->name = $name; + $this->type = $type; + $this->serializable = $serializable; + } + + /** + * @param \vhs\database\IGenerator $generator + * @param mixed|null $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var IColumnGenerator $generator */ + return $this->generateColumn($generator); + } + + /** + * generateColumn. + * + * @param \vhs\database\IColumnGenerator $generator + * + * @return string + */ + public function generateColumn(IColumnGenerator $generator) { + return $generator->generateColumn($this); + } + + /** + * getAbsoluteName. + * + * @return string + */ + public function getAbsoluteName() { + return $this->table->name . '.' . $this->name; + } + + /** + * Specify data which should be serialized to JSON. + * + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return string data which can be serialized by json_encode, + * which is a value of any type other than a resource + * + * @since 5.4.0 + */ + public function jsonSerialize(): string { + return $this->table->name . '.' . $this->name . '::' . get_class($this->type); + } + + /** + * String representation of object. + * + * @link http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or null + * + * @since 5.1.0 + */ + public function serialize(): string { + return $this->table->name . '.' . $this->name . '::' . get_class($this->type); + } + + /** + * Constructs the object. + * + * @link http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized

+ * The string representation of the object. + *

+ * + * @return void + * + * @since 5.1.0 + */ + public function unserialize($serialized): void { + // TODO: Implement unserialize() method. + } + + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + $this->type = clone $this->type; + } + + /** + * __serialize. + * + * @return mixed[] + */ + public function __serialize(): array { + return [$this->serialize()]; + } + + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ + public function __unserialize($data): void { + // TODO: Implement __unserialize() method. + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + $this->table = $table; + } +} diff --git a/packages/backend-php/vhs/database/Columns.php b/packages/backend-php/vhs/database/Columns.php new file mode 100644 index 00000000..31d56e1b --- /dev/null +++ b/packages/backend-php/vhs/database/Columns.php @@ -0,0 +1,151 @@ +__columns = $columns; + } + + /** + * compare Column $a to Column $b. + * + * @param Column $a + * @param Column $b + * + * @return bool + */ + public static function Equals(Column $a, Column $b) { + return $a->name === $b->name && $a->table === $b->table && $a->type === $b->type; + } + + /** + * Check if Column $a's name matches $name. + * + * @param Column $a + * @param string $name + * + * @return bool + */ + public static function EqualsByName(Column $a, $name) { + return $a->name === $name; + } + + /** + * add a column definition. + * + * @param Column $column + * + * @return \vhs\database\Column|null + */ + public function add(Column $column) { + if (!$this->contains($column->name)) { + array_push($this->__columns, $column); + + return $column; + } + + return null; + } + + /** + * @return Column[] + */ + public function all() { + return $this->__columns; + } + + /** + * check if Column set contains the specified column name. + * + * @param string $column + * + * @return bool + */ + public function contains($column) { + foreach ($this->__columns as $col) { + if ($col->name == $column) { + return true; + } + } + + return false; + } + + /** + * get a column by name. + * + * @param string $name + * + * @return \vhs\database\Column|null + */ + public function getByName($name) { + foreach ($this->__columns as $col) { + if ($col->name == $name) { + return $col; + } + } + + return null; + } + + /** + * get column names. + * + * @return string[] + */ + public function names() { + $names = []; + + foreach ($this->all() as $col) { + array_push($names, $col->name); + } + + return $names; + } + + /** + * remove a column. + * + * @param Column $column + * + * @return void + */ + public function remove(Column $column) { + if (!$this->contains($column->name)) { + return; + } + + foreach ($this->__columns as $key => $col) { + if (self::Equals($col, $column)) { + unset($this->__columns[$key]); + } + } + } + + /** + * column getter. + * + * @param string $name + * + * @return \vhs\database\Column|null + */ + public function __get($name) { + return $this->getByName($name); + } +} diff --git a/packages/backend-php/vhs/database/ConnectionInfo.php b/packages/backend-php/vhs/database/ConnectionInfo.php new file mode 100644 index 00000000..637f2d3b --- /dev/null +++ b/packages/backend-php/vhs/database/ConnectionInfo.php @@ -0,0 +1,29 @@ + + */ + abstract public function getDetails(); + + /** + * __toString. + * + * @return string + */ + public function __toString() { + return var_export($this->getDetails(), true); + } +} diff --git a/packages/backend-php/vhs/database/Database.php b/packages/backend-php/vhs/database/Database.php new file mode 100644 index 00000000..1c0506bf --- /dev/null +++ b/packages/backend-php/vhs/database/Database.php @@ -0,0 +1,314 @@ +setLoggerInternal(new SilentLogger()); + $this->setEngineInternal(new InMemoryEngine()); + $this->setRethrowInternal(true); + } + + /** + * __destruct. + * + * @return void + */ + public function __destruct() { + $this->engine->disconnect(); + } + + /** + * arbitrary. + * + * @param mixed $command + * + * @return mixed + */ + public static function arbitrary($command) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $command) { + // TODO warn that this is not ideal + return $db->engine->arbitrary($command); + }); + } + + /** + * count. + * + * @param \vhs\database\queries\QueryCount $query + * + * @return mixed + */ + public static function count(QueryCount $query) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $query) { + return $db->engine->count($query); + }); + } + + /** + * DateFormat. + * + * @return string + */ + public static function DateFormat() { + return self::getInstance()->engine->DateFormat(); + } + + /** + * delete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return mixed + */ + public static function delete(QueryDelete $query) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $query) { + return $db->engine->delete($query); + }); + } + + /** + * exists. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ + public static function exists(QuerySelect $query) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $query) { + return $db->engine->exists($query); + }); + } + + /** + * insert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return mixed + */ + public static function insert(QueryInsert $query) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $query) { + return $db->engine->insert($query); + }); + } + + /** + * scalar. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ + public static function scalar(QuerySelect $query) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $query) { + return $db->engine->scalar($query); + }); + } + + /** + * select. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ + public static function select(QuerySelect $query) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $query) { + return $db->engine->select($query); + }); + } + + /** + * setEngine. + * + * @param \vhs\database\Engine $engine + * + * @return void + */ + public static function setEngine(Engine $engine) { + /** @var Database $db */ + $db = self::getInstance(); + + $db->setEngineInternal($engine); + } + + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public static function setLogger(Logger $logger) { + $db = self::getInstance(); + + $db->setLoggerInternal($logger); + } + + /** + * setRethrow. + * + * @param mixed $rethrow + * + * @return void + */ + public static function setRethrow($rethrow) { + /** @var Database $db */ + $db = self::getInstance(); + + $db->setRethrowInternal($rethrow); + } + + /** + * update. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return mixed + */ + public static function update(QueryUpdate $query) { + /** @var Database $db */ + $db = self::getInstance(); + + return $db->invokeEngine(function () use ($db, $query) { + return $db->engine->update($query); + }); + } + + /** + * handleException. + * + * @param mixed $exception + * + * @return void + */ + private function handleException($exception) { + $this->logger->log($exception); + + if ($this->rethrow) { + throw $exception; + } + } + + /** + * invokeEngine. + * + * @param callable $func + * + * @return mixed + */ + private function invokeEngine(callable $func) { + try { + $this->engine->connect(); + } catch (DatabaseConnectionException $ex) { + $this->handleException($ex); + } + + $retval = null; + + try { + $retval = $func(); + } catch (DatabaseException $ex) { + $this->handleException($ex); + } + + return $retval; + } + + /** + * setEngineInternal. + * + * @param \vhs\database\Engine $engine + * + * @return void + */ + private function setEngineInternal(Engine $engine) { + if (!is_null($this->engine)) { + $this->engine->disconnect(); + } + + $this->engine = $engine; + } + + /** + * setLoggerInternal. + * + * @param \vhs\Logger $logger + * + * @return void + */ + private function setLoggerInternal(Logger $logger) { + $this->logger = $logger; + } + + /** + * setRethrowInternal. + * + * @param mixed $rethrow + * + * @return void + */ + private function setRethrowInternal($rethrow) { + $this->rethrow = $rethrow; + } +} diff --git a/packages/backend-php/vhs/database/Element.php b/packages/backend-php/vhs/database/Element.php new file mode 100644 index 00000000..0594fdef --- /dev/null +++ b/packages/backend-php/vhs/database/Element.php @@ -0,0 +1,27 @@ +where = $where; + } + + /** + * Where. + * + * @param \vhs\database\wheres\Where $where + * + * @return \vhs\database\On + */ + public static function Where(Where $where) { + return new On($where); + } + + /** + * @param \vhs\database\IOnGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + return $this->generateOn($generator); + } + + /** + * generateOn. + * + * @param \vhs\database\IOnGenerator $generator + * + * @return mixed + */ + public function generateOn(IOnGenerator $generator) { + return $generator->generateOn($this); + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + $this->where->__updateTable($table); + } +} diff --git a/packages/backend-php/vhs/database/Table.php b/packages/backend-php/vhs/database/Table.php new file mode 100644 index 00000000..6475733c --- /dev/null +++ b/packages/backend-php/vhs/database/Table.php @@ -0,0 +1,172 @@ +name = $name; + + if (is_null($alias)) { + // 'm gvn' hr ll sh's gt, cptn! + $alias = str_ireplace(['a', 'e', 'i', 'o', 'u'], '', $name); + } + + $this->aliasPrefix = $alias; + $this->alias = $alias . (string) self::$cloneIndex; + + if (is_null($this->joins)) { + $this->joins = []; + } + + foreach ($join as $j) { + array_push($this->joins, $j); + } + + $this->columns = new Columns(); + } + + /** + * add a column. + * + * @param string $name + * @param \vhs\database\types\Type $type + * @param bool $serializable + * + * @return \vhs\database\Column|null + */ + public function addColumn($name, Type $type, $serializable = true) { + return $this->columns->add(new Column($this, $name, $type, $serializable)); + } + + /** + * @param \vhs\database\IGenerator $generator + * @param mixed|null $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var ITableGenerator $generator */ + return $this->generateTable($generator); + } + + /** + * @param ITableGenerator $generator + * + * @return mixed + */ + public function generateTable(ITableGenerator $generator) { + return $generator->generateTable($this); + } + + /** + * @return PrimaryKey[] + */ + public function getPrimaryKeys() { + $pks = []; + foreach ($this->constraints as $constraint) { + if ($constraint instanceof PrimaryKey) { + array_push($pks, $constraint); + } + } + + return $pks; + } + + /** + * Undocumented function. + * + * @param \vhs\database\access\IAccess ...$checks + * + * @return void + */ + public function setAccess(IAccess ...$checks) { + $this->checks = $checks; + } + + /** + * set constraints. + * + * @param \vhs\database\constraints\Constraint ...$constraints + * + * @return void + */ + public function setConstraints(Constraint ...$constraints) { + $this->constraints = $constraints; + } + + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + parent::__clone(); + + self::$cloneIndex += 1; + + $this->alias = $this->aliasPrefix . (string) self::$cloneIndex; + + foreach ($this->joins as $join) { + $join->__updateTable($this); + } + + foreach ($this->columns->all() as $column) { + $column->__updateTable($this); + } + + foreach ($this->constraints as $constraint) { + $constraint->__updateTable($this); + } + } +} diff --git a/packages/backend-php/vhs/database/access/IAccess.php b/packages/backend-php/vhs/database/access/IAccess.php new file mode 100644 index 00000000..87af9c15 --- /dev/null +++ b/packages/backend-php/vhs/database/access/IAccess.php @@ -0,0 +1,38 @@ +column = $column; + } + + /** + * ForeignKey. + * + * @param \vhs\database\Column $column + * @param \vhs\database\Table $foreignTable + * @param \vhs\database\Column $on + * + * @return \vhs\database\constraints\ForeignKey + */ + public static function ForeignKey(Column $column, Table &$foreignTable, Column $on) { + return new ForeignKey($column, $foreignTable, $on); + } + + /** + * PrimaryKey. + * + * @param \vhs\database\Column $column + * + * @return \vhs\database\constraints\PrimaryKey + */ + public static function PrimaryKey(Column $column) { + return new PrimaryKey($column); + } + + /** + * generateConstraint. + * + * @param \vhs\database\constraints\IConstraintGenerator $generator + * + * @return mixed + */ + abstract public function generateConstraint(IConstraintGenerator $generator); + + /** + * @param \vhs\database\IGenerator $generator + * @param mixed|null $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var IConstraintGenerator $generator */ + return $this->generateConstraint($generator); + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + $this->column->__updateTable($table); + } +} diff --git a/vhs/database/constraints/ForeignKey.php b/packages/backend-php/vhs/database/constraints/ForeignKey.php similarity index 87% rename from vhs/database/constraints/ForeignKey.php rename to packages/backend-php/vhs/database/constraints/ForeignKey.php index 90f97c0a..f55fd99f 100644 --- a/vhs/database/constraints/ForeignKey.php +++ b/packages/backend-php/vhs/database/constraints/ForeignKey.php @@ -12,6 +12,7 @@ use vhs\database\Column; use vhs\database\Table; +/** @typescript */ class ForeignKey extends Constraint { /** @var Column */ public $on; @@ -29,7 +30,12 @@ public function generateConstraint(IConstraintGenerator $generator) { return $generator->generateForeignKey($this); } - public function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { $this->on = clone $this->on; } diff --git a/packages/backend-php/vhs/database/constraints/IConstraintGenerator.php b/packages/backend-php/vhs/database/constraints/IConstraintGenerator.php new file mode 100644 index 00000000..12c8e4bd --- /dev/null +++ b/packages/backend-php/vhs/database/constraints/IConstraintGenerator.php @@ -0,0 +1,33 @@ +generatePrimaryKey($this); diff --git a/packages/backend-php/vhs/database/engines/memory/InMemoryEngine.php b/packages/backend-php/vhs/database/engines/memory/InMemoryEngine.php new file mode 100644 index 00000000..d0ecae67 --- /dev/null +++ b/packages/backend-php/vhs/database/engines/memory/InMemoryEngine.php @@ -0,0 +1,318 @@ + + */ + private $datastore = []; + + /** + * generator. + * + * @var mixed + */ + private $generator; + + /** + * keyIncrementors. + * + * @var array + */ + private $keyIncrementors = []; + + /** + * logger. + * + * @var \vhs\Logger + */ + private $logger; + + /** + * __construct. + * + * @return void + */ + public function __construct() { + $this->logger = new SilentLogger(); + + $this->generator = new InMemoryGenerator(); + } + + /** + * connect. + * + * @return bool + */ + public function connect() { + return true; //if we can't "connect" to memory but got this far, I don't know whatever + } + + /** + * disconnect. + * + * @return bool + */ + public function disconnect() { + $this->keyIncrementors = []; + $this->datastore = []; + $this->logger->log('disconnected'); + + return true; // ha + } + + /** + * DateFormat. + * + * @return string + */ + public static function DateFormat() { + return 'Y-m-d H:i:s'; + } + + /** + * arbitrary. + * + * @param mixed $command + * + * @return bool + */ + public function arbitrary($command) { + return false; + } + + /** + * count. + * + * @param \vhs\database\queries\QueryCount $query + * + * @return bool|int + */ + public function count(QueryCount $query) { + $this->logger->log('count ' . $query->table->name . ' ' . $query->where); + if (!array_key_exists($query->table->name, $this->datastore)) { + return false; + } + + $match = !is_null($query->where) + ? $query->where->generate($this->generator) + : function ($item) { + return true; + }; + + // TODO currently not in use, always false: remove or implement + // if (isset($orderBy) || isset($limit)) { + // throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); + // } + + $count = 0; + + foreach ($this->datastore[$query->table->name] as $row) { + if ($match($row)) { + $count += 1; + } + } + + return $count; + } + + /** + * delete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return bool + */ + public function delete(QueryDelete $query) { + $this->logger->log('delete ' . $query->table->name . ' ' . $query->where); + if (!array_key_exists($query->table->name, $this->datastore)) { + return false; + } + + $match = !is_null($query->where) + ? $query->where->generate($this->generator) + : function ($item) { + return true; + }; + + foreach ($this->datastore[$query->table->name] as $key => $row) { + if ($match($row)) { + unset($this->datastore[$query->table->name][$key]); + } + } + + return true; + } + + /** + * exists. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return bool + */ + public function exists(QuerySelect $query) { + $this->logger->log('exists: '); + + return $this->count(new QueryCount($query->table, $query->where, $query->orderBy, $query->limit, $query->offset)) > 0; + } + + /** + * insert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return mixed + */ + public function insert(QueryInsert $query) { + $this->logger->log('insert ' . $query->table->name . ' ' . var_export($query->values, true)); + if (!array_key_exists($query->table->name, $this->datastore)) { + $this->datastore[$query->table->name] = []; + } + + $pks = []; + + foreach ($query->table->getPrimaryKeys() as $pk) { + if (!array_key_exists($pk->column->name, $query->values)) { + $key = $query->table->name . '.' . $pk->column->name; + if (!array_key_exists($key, $this->keyIncrementors)) { + $this->keyIncrementors[$key] = 0; + } + $this->keyIncrementors[$key] += 1; + $pks[$pk->column->name] = $query->values[$pk->column->name] = $this->keyIncrementors[$key]; + } + } + + array_push($this->datastore[$query->table->name], $query->values); + + if (count($pks) == 1) { + return array_values($pks)[0]; + } + + return $pks; + } + + /** + * scalar. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ + public function scalar(QuerySelect $query) { + $this->logger->log('scalar: '); + + $record = $this->select($query); + + if (sizeof($record) != 1) { + return null; + } else { + return $record[$query->columns[0]->name]; + } + } + + /** + * select. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return mixed + */ + public function select(QuerySelect $query) { + $this->logger->log('select ' . $query->table->name . ' ' . implode(', ', $query->columns->names()) . ' ' . $query->where); + + if (!array_key_exists($query->table->name, $this->datastore)) { + return []; + } + + $match = !is_null($query->where) + ? $query->where->generate($this->generator) + : function () { + return true; + }; + + // TODO currently not in use, always false: remove or implement + // if (isset($orderBy) || isset($limit)) { + // throw new \Exception('TODO implement OrderBy and limit for InMemoryEngine'); + // } + + $results = []; + + $cols = []; + + foreach ($query->columns->all() as $column) { + array_push($cols, $column->name); + } + + foreach ($this->datastore[$query->table->name] as $row) { + if ($match($row)) { + array_push($results, array_intersect_key($row, array_fill_keys($cols, false))); + } + } + + $this->logger->log(var_export($results, true)); + + return $results; + } + + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public function setLogger(Logger $logger) { + $this->logger = $logger; + } + + /** + * update. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return bool + */ + public function update(QueryUpdate $query) { + $this->logger->log('update ' . $query->table->name . ' ' . var_export($query->values, true) . ' ' . $query->where); + if (!array_key_exists($query->table->name, $this->datastore)) { + return false; + } + + $match = !is_null($query->where) + ? $query->where->generate($this->generator) + : function ($item) { + return true; + }; + + foreach ($this->datastore[$query->table->name] as $key => $row) { + if ($match($row)) { + foreach ($query->values as $column => $value) { + $this->datastore[$query->table->name][$key][$column] = $value; + } + } + } + + return true; + } +} diff --git a/packages/backend-php/vhs/database/engines/memory/InMemoryGenerator.php b/packages/backend-php/vhs/database/engines/memory/InMemoryGenerator.php new file mode 100644 index 00000000..9c19242f --- /dev/null +++ b/packages/backend-php/vhs/database/engines/memory/InMemoryGenerator.php @@ -0,0 +1,522 @@ +wheres as $w) { + array_push($wheres, $w->generate($this)); + } + + /** + * lambda. + * + * @return bool + */ + return function ($row) use ($wheres) { + $b = true; + + foreach ($wheres as $w) { + $b = $b && $w($row); + } + + return $b; + }; + } + + /** + * generateAscending. + * + * @param \vhs\database\orders\OrderByAscending $ascending + * + * @return void + */ + public function generateAscending(OrderByAscending $ascending) { + // TODO: Implement generateAscending() method. + throw new \Exception('TODO: Implement generateAscending() method.'); + } + + /** + * generateBool. + * + * @param \vhs\database\types\TypeBool $type + * @param mixed $value + * + * @return void + */ + public function generateBool(TypeBool $type, mixed $value = null) { + // TODO: Implement generateBool() method. + } + + /** + * generateColumn. + * + * @param \vhs\database\Column $column + * + * @return void + */ + public function generateColumn(Column $column) { + // TODO: Implement generateColumn() method. + } + + /** + * generateComparator. + * + * @param \vhs\database\wheres\WhereComparator $where + * + * @return callable + */ + public function generateComparator(WhereComparator $where) { + return function ($row) use ($where) { + $column = $where->column->name; + $value = $where->value; + + if (!array_key_exists($column, $row)) { + return []; + } + + $item = $row[$column]; + + if ($where->isArray) { + if (in_array($item, $value)) { + if ($where->equal) { + return true; + } + } else { + if (!$where->equal) { + return true; + } + } + + return false; + } + + if ($where->lesser) { + if ($where->equal) { + if ($item <= $value) { + return true; + } + } else { + if ($item < $value) { + return true; + } + } + + return false; + } + + if ($where->greater) { + if ($where->equal) { + if ($item >= $value) { + return true; + } + } else { + if ($item > $value) { + return true; + } + } + + return false; + } + + if ($where->null_compare) { + if ($where->equal) { + if (is_null($item)) { + return true; + } + } else { + if (!is_null($item)) { + return true; + } + } + + return false; + } + + if ($where->equal) { + if ($item === $value) { + return true; + } + } else { + if ($item !== $value) { + return true; + } + } + + if ($where->like) { + return strpos($item, $value) != false; + } + + return false; + }; + } + + /** + * generateCross. + * + * @param \vhs\database\joins\JoinCross $join + * + * @return void + */ + public function generateCross(JoinCross $join) { + // TODO: Implement generateCross() method. + } + + /** + * generateDate. + * + * @param \vhs\database\types\TypeDate $type + * @param mixed $value + * + * @return void + */ + public function generateDate(TypeDate $type, mixed $value = null) { + // TODO: Implement generateDate() method. + } + + /** + * generateDateTime. + * + * @param \vhs\database\types\TypeDateTime $type + * @param mixed $value + * + * @return void + */ + public function generateDateTime(TypeDateTime $type, mixed $value = null) { + // TODO: Implement generateDateTime() method. + } + + /** + * generateDelete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return void + */ + public function generateDelete(QueryDelete $query) { + // TODO: Implement generateDelete() method. + } + + /** + * generateDescending. + * + * @param \vhs\database\orders\OrderByDescending $descending + * + * @return void + */ + public function generateDescending(OrderByDescending $descending) { + // TODO: Implement generateDescending() method. + throw new \Exception('Implement generateDescending() method.'); + } + + /** + * generateEnum. + * + * @param \vhs\database\types\TypeEnum $type + * @param mixed $value + * + * @return void + */ + public function generateEnum(TypeEnum $type, mixed $value = null) { + // TODO: Implement generateEnum() method. + } + + /** + * generateFloat. + * + * @param \vhs\database\types\TypeFloat $type + * @param mixed $value + * + * @return void + */ + public function generateFloat(TypeFloat $type, mixed $value = null) { + // TODO: Implement generateFloat() method. + } + + /** + * generateForeignKey. + * + * @param \vhs\database\constraints\ForeignKey $constraint + * + * @return void + */ + public function generateForeignKey(ForeignKey $constraint) { + // TODO: Implement generateForeignKey() method. + } + + /** + * generateInner. + * + * @param \vhs\database\joins\JoinInner $join + * + * @return void + */ + public function generateInner(JoinInner $join) { + // TODO: Implement generateInner() method. + } + + /** + * generateInsert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return void + */ + public function generateInsert(QueryInsert $query) { + // TODO: Implement generateInsert() method. + } + + /** + * generateInt. + * + * @param \vhs\database\types\TypeInt $type + * @param mixed $value + * + * @return void + */ + public function generateInt(TypeInt $type, mixed $value = null) { + // TODO: Implement generateInt() method. + } + + /** + * generateLeft. + * + * @param \vhs\database\joins\JoinLeft $join + * + * @return void + */ + public function generateLeft(JoinLeft $join) { + // TODO: Implement generateLeft() method. + } + + /** + * generateLimit. + * + * @param \vhs\database\limits\Limit $limit + * + * @return int + */ + public function generateLimit(Limit $limit) { + return $limit->limit; + } + + /** + * generateOffset. + * + * @param Offset $offset + * + * @return mixed + */ + public function generateOffset(Offset $offset) { + return $offset->offset; + } + + /** + * generateOn. + * + * @param On $on + * + * @return void + */ + public function generateOn(On $on) { + // TODO: Implement generateOn() method. + } + + /** + * generateOr. + * + * @param WhereOr $where + * + * @return callable + */ + public function generateOr(WhereOr $where) { + $wheres = []; + + /** @var Where $w */ + foreach ($where->wheres as $w) { + array_push($wheres, $w->generate($this)); + } + + return function ($row) use ($wheres) { + $b = false; + foreach ($wheres as $w) { + $b = $b || $w($row); + } + + return $b; + }; + } + + /** + * generateOuter. + * + * @param JoinOuter $join + * + * @return void + */ + public function generateOuter(JoinOuter $join) { + // TODO: Implement generateOuter() method. + } + + /** + * generatePrimaryKey. + * + * @param PrimaryKey $constraint + * + * @return void + */ + public function generatePrimaryKey(PrimaryKey $constraint) { + // TODO: Implement generatePrimaryKey() method. + } + + /** + * generateRight. + * + * @param JoinRight $join + * + * @return void + */ + public function generateRight(JoinRight $join) { + // TODO: Implement generateRight() method. + } + + /** + * generateSelect. + * + * @param QuerySelect $query + * + * @return void + */ + public function generateSelect(QuerySelect $query) { + // TODO: Implement generateSelect() method. + } + + /** + * generateSelectCount. + * + * @param QueryCount $query + * + * @return void + */ + public function generateSelectCount(QueryCount $query) { + // TODO: Implement generateSelect() method. + } + + /** + * generateString. + * + * @param TypeString $type + * @param mixed $value + * + * @return void + */ + public function generateString(TypeString $type, $value = null) { + // TODO: Implement generateString() method. + } + + /** + * generateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function generateTable(Table $table) { + // TODO: Implement generateTable() method. + } + + /** + * generateText. + * + * @param \vhs\database\types\TypeText $type + * @param mixed $value + * + * @return void + */ + public function generateText(TypeText $type, mixed $value = null) { + // TODO: Implement generateText() method. + } + + /** + * generateUpdate. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return void + */ + public function generateUpdate(QueryUpdate $query) { + // TODO: Implement generateUpdate() method. + } +} diff --git a/packages/backend-php/vhs/database/engines/mysql/MySqlConnectionInfo.php b/packages/backend-php/vhs/database/engines/mysql/MySqlConnectionInfo.php new file mode 100644 index 00000000..9cf93c03 --- /dev/null +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlConnectionInfo.php @@ -0,0 +1,112 @@ +server = $server; + $this->username = $username; + $this->password = $password; + $this->database = $database; + + // TODO throw argument exceptions here if shit is rotten + } + + /** + * getDatabase. + * + * @return string + */ + public function getDatabase() { + return $this->database; + } + + /** + * getDetails. + * + * @return array{server:string,username:string,password:string,database:string} + */ + public function getDetails() { + return [ + 'server' => $this->server, + 'username' => $this->username, + 'password' => $this->password, + 'database' => $this->database + ]; + } + + /** + * getPassword. + * + * @return string + */ + public function getPassword() { + return $this->password; + } + + /** + * getServer. + * + * @return string + */ + public function getServer() { + return $this->server; + } + + /** + * getUsername. + * + * @return string + */ + public function getUsername() { + return $this->username; + } +} diff --git a/packages/backend-php/vhs/database/engines/mysql/MySqlConverter.php b/packages/backend-php/vhs/database/engines/mysql/MySqlConverter.php new file mode 100644 index 00000000..c90d6950 --- /dev/null +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlConverter.php @@ -0,0 +1,187 @@ +nullable) { + return $type->default; + } else { + return null; + } + } + + return boolval($value); + } + + /** + * convertDate. + * + * @param \vhs\database\types\TypeDate $type + * @param string|null $value + * + * @return string|null + */ + public function convertDate(TypeDate $type, $value = null) { + if (is_null($value)) { + if (!$type->nullable) { + return $type->default; + } else { + return null; + } + } + + return date('Y-m-d', strtotime(str_replace('-', '/', $value))); + } + + /** + * convertDateTime. + * + * @param \vhs\database\types\TypeDateTime $type + * @param string|null $value + * + * @return string|null + */ + public function convertDateTime(TypeDateTime $type, $value = null) { + if (is_null($value) || empty($value)) { + if (!$type->nullable) { + return $type->default; + } else { + return null; + } + } + + return date('Y-m-d H:i:s', strtotime(str_replace('-', '/', $value))); + } + + /** + * convertEnum. + * + * @param \vhs\database\types\TypeEnum $type + * @param string|null $value + * + * @return string|null + */ + public function convertEnum(TypeEnum $type, $value = null) { + if (is_null($value)) { + if (!$type->nullable) { + return $type->default; + } else { + return null; + } + } + + if (in_array($value, $type->values)) { + return (string) $value; + } else { + throw new \Exception("Invalid enum value {$value} does not exist"); + } + } + + /** + * convertFloat. + * + * @param \vhs\database\types\TypeFloat $type + * @param float|null $value + * + * @return float|null + */ + public function convertFloat(TypeFloat $type, $value = null) { + if (is_null($value)) { + if (!$type->nullable) { + return $type->default; + } else { + return null; + } + } + + return floatval($value); + } + + /** + * convertInt. + * + * @param \vhs\database\types\TypeInt $type + * @param int|null $value + * + * @return int|null + */ + public function convertInt(TypeInt $type, $value = null) { + if (is_null($value)) { + if (!$type->nullable) { + return $type->default; + } else { + return null; + } + } + + return intval($value); + } + + /** + * convertString. + * + * @param \vhs\database\types\TypeString $type + * @param string|null $value + * + * @return string|null + */ + public function convertString(TypeString $type, $value = null) { + if (is_null($value)) { + if (!$type->nullable) { + return $type->default; + } else { + return null; + } + } + + return (string) $value; + } + + /** + * convertText. + * + * @param \vhs\database\types\TypeText $type + * @param string|null $value + * + * @return string|null + */ + public function convertText(TypeText $type, $value = null) { + if (is_null($value)) { + if (!$type->nullable) { + return $type->default; + } else { + return null; + } + } + + return (string) $value; + } +} diff --git a/packages/backend-php/vhs/database/engines/mysql/MySqlEngine.php b/packages/backend-php/vhs/database/engines/mysql/MySqlEngine.php new file mode 100644 index 00000000..b4e6b16b --- /dev/null +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlEngine.php @@ -0,0 +1,342 @@ +info = $connectionInfo; + $this->autoCreateDatabase = $autoCreateDatabase; + + $this->logger = new SilentLogger(); + + $this->generator = new MySqlGenerator(); + $this->converter = new MySqlConverter(); + } + + /** + * connect. + * + * @throws \vhs\database\exceptions\DatabaseConnectionException + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ + public function connect() { + if (isset($this->conn) && !is_null($this->conn)) { + return true; + } + + $this->conn = new \mysqli($this->info->getServer(), $this->info->getUsername(), $this->info->getPassword()); + + if ($this->conn->connect_error) { + throw new DatabaseConnectionException($this->conn->connect_error); + } + + if ($this->autoCreateDatabase) { + $sql = "CREATE DATABASE IF NOT EXISTS {$this->info->getDatabase()};"; + if ($this->conn->query($sql) !== true) { + throw new DatabaseException($this->conn->error); + } + } + + $this->conn->select_db($this->info->getDatabase()); + + $this->generator->SetMySqli($this->conn); + + return true; + } + + /** + * disconnect. + * + * @return void + */ + public function disconnect() { + if (isset($this->conn) && !is_null($this->conn)) { + $this->conn->close(); + } + + unset($this->conn); + } + + /** + * DateFormat. + * + * @return string + */ + public static function DateFormat() { + return 'Y-m-d H:i:s'; + } + + /** + * arbitrary. + * + * @param mixed $command + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ + public function arbitrary($command) { + $this->logger->log('[ARBITRARY] ' . $command); + + $rows = []; + if ($q = $this->conn->query($command)) { + $rows = $q->fetch_all(); + $q->close(); + + return $rows; + } + + throw new DatabaseException($this->conn->error); + } + + /** + * count. + * + * @param \vhs\database\queries\QueryCount $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return int|null + */ + public function count(QueryCount $query) { + $sql = $query->generate($this->generator); + + $this->logger->log($sql); + + if ($q = $this->conn->query($sql)) { + $rows = $q->fetch_all(); + $row = $rows[0]; + $q->close(); + + if (sizeof($row) != 1) { + return null; + } + + return $row[0]; + } else { + throw new DatabaseException($this->conn->error); + } + } + + /** + * delete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ + public function delete(QueryDelete $query) { + $sql = $query->generate($this->generator); + + $this->logger->log($sql); + + if ($q = $this->conn->query($sql)) { + return true; + } + + throw new DatabaseException($this->conn->error); + } + + /** + * exists. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ + public function exists(QuerySelect $query) { + return $this->count(new QueryCount($query->table, $query->where, $query->orderBy, $query->limit, $query->offset)) > 0; + } + + /** + * insert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ + public function insert(QueryInsert $query) { + $sql = $query->generate($this->generator); + + $this->logger->log($sql); + + if ($q = $this->conn->query($sql)) { + return $this->conn->insert_id; + } + + throw new DatabaseException($this->conn->error); + } + + /** + * scalar. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ + public function scalar(QuerySelect $query) { + $row = $this->select($query); + + if (sizeof($row) != 1) { + return null; + } + + return $row[0][$query->columns->all()[0]->name]; + } + + /** + * select. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return mixed + */ + public function select(QuerySelect $query) { + $sql = $query->generate($this->generator); + + $this->logger->log($sql); + + $records = []; + + if ($q = $this->conn->query($sql)) { + $rows = $q->fetch_all(); + + foreach ($rows as $row) { + $record = array_combine($query->columns->names(), $row); + + /* TODO fix potential bug with joins having same column names but are namespaced + * ie alias0.col alias1.col + * it's entirely possible the mysqli garbage doesn't support this. I haven't bothered to check yet. + */ + /** @var Column $col */ + foreach ($query->columns->all() as $col) { + if (array_key_exists($col->name, $record) && !is_null($record[$col->name])) { + $record[$col->name] = $col->type->convert($this->converter, $record[$col->name]); + } + } + + array_push($records, $record); + } + + $q->close(); + } else { + throw new DatabaseException($this->conn->error); + } + + return $records; + } + + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public function setLogger(Logger $logger) { + $this->logger = $logger; + } + + /** + * update. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return bool + */ + public function update(QueryUpdate $query) { + $sql = $query->generate($this->generator); + + $this->logger->log($sql); + + if ($q = $this->conn->query($sql)) { + return true; + } + + throw new DatabaseException($this->conn->error); + } +} diff --git a/vhs/database/engines/mysql/MySqlGenerator.php b/packages/backend-php/vhs/database/engines/mysql/MySqlGenerator.php similarity index 76% rename from vhs/database/engines/mysql/MySqlGenerator.php rename to packages/backend-php/vhs/database/engines/mysql/MySqlGenerator.php index 938c6f33..030e24e6 100644 --- a/vhs/database/engines/mysql/MySqlGenerator.php +++ b/packages/backend-php/vhs/database/engines/mysql/MySqlGenerator.php @@ -30,7 +30,6 @@ use vhs\database\orders\OrderByAscending; use vhs\database\orders\OrderByDescending; use vhs\database\queries\IQueryGenerator; -use vhs\database\queries\Query; use vhs\database\queries\QueryCount; use vhs\database\queries\QueryDelete; use vhs\database\queries\QueryInsert; @@ -52,6 +51,7 @@ use vhs\database\wheres\WhereComparator; use vhs\database\wheres\WhereOr; +/** @typescript */ class MySqlGenerator implements IWhereGenerator, IOrderByGenerator, @@ -62,11 +62,16 @@ class MySqlGenerator implements IJoinGenerator, IOnGenerator, IColumnGenerator { - /** - * @var \mysqli - */ + /** @var \mysqli */ private $conn = null; + /** + * generateAnd. + * + * @param \vhs\database\wheres\WhereAnd $where + * + * @return string + */ public function generateAnd(WhereAnd $where) { $sql = '('; @@ -82,10 +87,25 @@ public function generateAnd(WhereAnd $where) { return $sql; } + /** + * generateAscending. + * + * @param \vhs\database\orders\OrderByAscending $ascending + * + * @return string + */ public function generateAscending(OrderByAscending $ascending) { return $this->gen($ascending, 'ASC'); } + /** + * generateBool. + * + * @param \vhs\database\types\TypeBool $type + * @param bool|null $value + * + * @return string + */ public function generateBool(TypeBool $type, $value = null) { return $this->genVal( function ($val) { @@ -100,10 +120,24 @@ function ($val) { ); } + /** + * generateColumn. + * + * @param \vhs\database\Column $column + * + * @return string + */ public function generateColumn(Column $column) { return "{$column->table->alias}.`{$column->name}`"; } + /** + * generateComparator. + * + * @param \vhs\database\wheres\WhereComparator $where + * + * @return string + */ public function generateComparator(WhereComparator $where) { if ($where->isArray || (is_object($where->value) && get_class($where->value) == 'vhs\\database\\queries\\QuerySelect')) { $sql = $where->column->generate($this); @@ -169,10 +203,25 @@ public function generateComparator(WhereComparator $where) { } } + /** + * generateCross. + * + * @param \vhs\database\joins\JoinCross $join + * + * @return string + */ public function generateCross(JoinCross $join) { return "CROSS JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateDate. + * + * @param \vhs\database\types\TypeDate $type + * @param string|null $value + * + * @return string + */ public function generateDate(TypeDate $type, $value = null) { return $this->genVal( function ($val) { @@ -183,6 +232,14 @@ function ($val) { ); } + /** + * generateDateTime. + * + * @param \vhs\database\types\TypeDateTime $type + * @param string|null $value + * + * @return string + */ public function generateDateTime(TypeDateTime $type, $value = null) { return $this->genVal( function ($val) { @@ -193,6 +250,13 @@ function ($val) { ); } + /** + * generateDelete. + * + * @param \vhs\database\queries\QueryDelete $query + * + * @return string + */ public function generateDelete(QueryDelete $query) { $clause = !is_null($query->where) ? $query->where->generate($this) : ''; @@ -205,10 +269,27 @@ public function generateDelete(QueryDelete $query) { return $sql; } + /** + * generateDescending. + * + * @param \vhs\database\orders\OrderByDescending $descending + * + * @return string + */ public function generateDescending(OrderByDescending $descending) { return $this->gen($descending, 'DESC'); } + /** + * generateEnum. + * + * @param \vhs\database\types\TypeEnum $type + * @param string|null $value + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return string + */ public function generateEnum(TypeEnum $type, $value = null) { return $this->genVal( function ($val) use ($type) { @@ -225,6 +306,14 @@ function ($val) use ($type) { ); } + /** + * generateFloat. + * + * @param \vhs\database\types\TypeFloat $type + * @param float|null $value + * + * @return string + */ public function generateFloat(TypeFloat $type, $value = null) { return $this->genVal( function ($val) { @@ -235,10 +324,24 @@ function ($val) { ); } + /** + * generateInner. + * + * @param \vhs\database\joins\JoinInner $join + * + * @return string + */ public function generateInner(JoinInner $join) { return "INNER JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateInsert. + * + * @param \vhs\database\queries\QueryInsert $query + * + * @return string + */ public function generateInsert(QueryInsert $query) { $columns = []; $values = []; @@ -257,6 +360,14 @@ public function generateInsert(QueryInsert $query) { return $sql; } + /** + * generateInt. + * + * @param \vhs\database\types\TypeInt $type + * @param mixed $value + * + * @return string + */ public function generateInt(TypeInt $type, $value = null) { return $this->genVal( function ($val) { @@ -267,30 +378,67 @@ function ($val) { ); } + /** + * generateLeft. + * + * @param \vhs\database\joins\JoinLeft $join + * + * @return string + */ public function generateLeft(JoinLeft $join) { return "LEFT JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateLimit. + * + * @param \vhs\database\limits\Limit $limit + * + * @return string + */ public function generateLimit(Limit $limit) { $clause = ''; if (isset($limit->limit) && is_numeric($limit->limit)) { $clause = sprintf(' LIMIT %s ', intval($limit->limit)); } + return $clause; } + /** + * generateOffset. + * + * @param \vhs\database\offsets\Offset $offset + * + * @return string + */ public function generateOffset(Offset $offset) { $clause = ''; if (isset($offset->offset) && is_numeric($offset->offset)) { $clause = sprintf(' OFFSET %s ', intval($offset->offset)); } + return $clause; } + /** + * generateOn. + * + * @param \vhs\database\On $on + * + * @return string + */ public function generateOn(On $on) { return '(' . $on->where->generate($this) . ')'; } + /** + * generateOr. + * + * @param \vhs\database\wheres\WhereOr $where + * + * @return string + */ public function generateOr(WhereOr $where) { $sql = '('; @@ -306,14 +454,35 @@ public function generateOr(WhereOr $where) { return $sql; } + /** + * generateOuter. + * + * @param \vhs\database\joins\JoinOuter $join + * + * @return string + */ public function generateOuter(JoinOuter $join) { return "OUTER JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateRight. + * + * @param \vhs\database\joins\JoinRight $join + * + * @return string + */ public function generateRight(JoinRight $join) { return "RIGHT JOIN {$join->table->name} {$join->table->alias} ON " . $join->on->generate($this); } + /** + * generateSelect. + * + * @param \vhs\database\queries\QuerySelect $query + * + * @return string + */ public function generateSelect(QuerySelect $query) { $selector = implode( ', ', @@ -329,7 +498,7 @@ public function generateSelect(QuerySelect $query) { $sql = "SELECT {$selector} FROM `{$query->table->name}` AS {$query->table->alias}"; if (!is_null($query->joins)) { - /** @var Join $join */ + /** @var \vhs\database\joins\Join $join */ foreach ($query->joins as $join) { $sql .= ' ' . $join->generate($this); } @@ -354,6 +523,13 @@ public function generateSelect(QuerySelect $query) { return $sql; } + /** + * generateSelectCount. + * + * @param \vhs\database\queries\QueryCount $query + * + * @return string + */ public function generateSelectCount(QueryCount $query) { $clause = !is_null($query->where) ? $query->where->generate($this) : ''; $orderClause = !is_null($query->orderBy) ? $query->orderBy->generate($this) : ''; @@ -388,10 +564,21 @@ public function generateSelectCount(QueryCount $query) { return $sql; } + /** + * generateString. + * + * @param \vhs\database\types\TypeString $type + * @param mixed $value + * + * @throws \vhs\database\exceptions\DatabaseException + * + * @return string + */ public function generateString(TypeString $type, $value = null) { return $this->genVal( function ($val) use ($type) { $v = (string) $val; + if (strlen($v) > $type->length) { throw new DatabaseException("Value of Type::String exceeds defined length of {$type->length}"); } @@ -403,8 +590,17 @@ function ($val) use ($type) { ); } + /** + * generateText. + * + * @param \vhs\database\types\TypeText $type + * @param mixed $value + * + * @return string + */ public function generateText(TypeText $type, $value = null) { return $this->genVal( + // @phpstan-ignore closure.unusedUse function ($val) use ($type) { return (string) $val; }, @@ -413,6 +609,13 @@ function ($val) use ($type) { ); } + /** + * generateUpdate. + * + * @param \vhs\database\queries\QueryUpdate $query + * + * @return string + */ public function generateUpdate(QueryUpdate $query) { $clause = !is_null($query->where) ? $query->where->generate($this) : ''; $setsql = implode( @@ -420,6 +623,7 @@ public function generateUpdate(QueryUpdate $query) { array_map( function ($columnName, $value) use ($query) { $column = $query->columns->getByName($columnName); + return $column->generate($this) . ' = ' . $column->type->generate($this, $value); }, array_keys($query->values), @@ -442,11 +646,21 @@ function ($columnName, $value) use ($query) { * is on the db to figure escaping. * * @param \mysqli $conn + * + * @return void */ public function SetMySqli(\mysqli $conn) { $this->conn = $conn; } + /** + * gen. + * + * @param OrderBy $orderBy + * @param mixed $type + * + * @return string + */ private function gen(OrderBy $orderBy, $type) { $clause = $orderBy->column->generate($this) . " {$type}, "; @@ -460,6 +674,15 @@ private function gen(OrderBy $orderBy, $type) { return $clause; } + /** + * genVal. + * + * @param callable $gen + * @param Type $type + * @param mixed $value + * + * @return string + */ private function genVal(callable $gen, Type $type, $value = null) { if (is_null($value)) { if ($type->nullable) { diff --git a/vhs/database/exceptions/DatabaseConnectionException.php b/packages/backend-php/vhs/database/exceptions/DatabaseConnectionException.php similarity index 91% rename from vhs/database/exceptions/DatabaseConnectionException.php rename to packages/backend-php/vhs/database/exceptions/DatabaseConnectionException.php index 3bd15cdb..f11e71fe 100644 --- a/vhs/database/exceptions/DatabaseConnectionException.php +++ b/packages/backend-php/vhs/database/exceptions/DatabaseConnectionException.php @@ -9,5 +9,6 @@ namespace vhs\database\exceptions; +/** @typescript */ class DatabaseConnectionException extends DatabaseException { } diff --git a/vhs/database/exceptions/DatabaseException.php b/packages/backend-php/vhs/database/exceptions/DatabaseException.php similarity index 90% rename from vhs/database/exceptions/DatabaseException.php rename to packages/backend-php/vhs/database/exceptions/DatabaseException.php index 7600dcbc..99be7489 100644 --- a/vhs/database/exceptions/DatabaseException.php +++ b/packages/backend-php/vhs/database/exceptions/DatabaseException.php @@ -9,5 +9,6 @@ namespace vhs\database\exceptions; +/** @typescript */ class DatabaseException extends \Exception { } diff --git a/vhs/database/exceptions/InvalidSchemaException.php b/packages/backend-php/vhs/database/exceptions/InvalidSchemaException.php similarity index 90% rename from vhs/database/exceptions/InvalidSchemaException.php rename to packages/backend-php/vhs/database/exceptions/InvalidSchemaException.php index aaabbc1c..168130bf 100644 --- a/vhs/database/exceptions/InvalidSchemaException.php +++ b/packages/backend-php/vhs/database/exceptions/InvalidSchemaException.php @@ -9,5 +9,6 @@ namespace vhs\database\exceptions; +/** @typescript */ class InvalidSchemaException extends \Exception { } diff --git a/packages/backend-php/vhs/database/joins/IJoinGenerator.php b/packages/backend-php/vhs/database/joins/IJoinGenerator.php new file mode 100644 index 00000000..808c5fec --- /dev/null +++ b/packages/backend-php/vhs/database/joins/IJoinGenerator.php @@ -0,0 +1,60 @@ +table = clone $table; + $this->on = $on; + $this->on->__updateTable($this->table); + } + + /** + * Cross. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinCross + */ + public static function Cross(Table $table, On $on) { + return new JoinCross($table, $on); + } + + /** + * Inner. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinInner + */ + public static function Inner(Table $table, On $on) { + return new JoinInner($table, $on); + } + + /** + * Left. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinLeft + */ + public static function Left(Table $table, On $on) { + return new JoinLeft($table, $on); + } + + /** + * Outer. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinOuter + */ + public static function Outer(Table $table, On $on) { + return new JoinOuter($table, $on); + } + + /** + * Right. + * + * @param \vhs\database\Table $table + * @param \vhs\database\On $on + * + * @return \vhs\database\joins\JoinRight + */ + public static function Right(Table $table, On $on) { + return new JoinRight($table, $on); + } + + /** + * generateJoin. + * + * @param \vhs\database\joins\IJoinGenerator $generator + * + * @return mixed + */ + abstract public function generateJoin(IJoinGenerator $generator); + + /** + * generate. + * + * @param \vhs\database\IGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var IJoinGenerator $generator */ + return $this->generateJoin($generator); + } + + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + $this->on = clone $this->on; + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + $this->table = $table; + $this->on->__updateTable($table); + } +} diff --git a/vhs/database/joins/JoinCross.php b/packages/backend-php/vhs/database/joins/JoinCross.php similarity index 93% rename from vhs/database/joins/JoinCross.php rename to packages/backend-php/vhs/database/joins/JoinCross.php index 40995e3e..e66e0517 100644 --- a/vhs/database/joins/JoinCross.php +++ b/packages/backend-php/vhs/database/joins/JoinCross.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinCross extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateCross($this); diff --git a/vhs/database/joins/JoinInner.php b/packages/backend-php/vhs/database/joins/JoinInner.php similarity index 93% rename from vhs/database/joins/JoinInner.php rename to packages/backend-php/vhs/database/joins/JoinInner.php index ce1902af..b5e9724c 100644 --- a/vhs/database/joins/JoinInner.php +++ b/packages/backend-php/vhs/database/joins/JoinInner.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinInner extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateInner($this); diff --git a/vhs/database/joins/JoinLeft.php b/packages/backend-php/vhs/database/joins/JoinLeft.php similarity index 93% rename from vhs/database/joins/JoinLeft.php rename to packages/backend-php/vhs/database/joins/JoinLeft.php index 6099de52..81eb22d5 100644 --- a/vhs/database/joins/JoinLeft.php +++ b/packages/backend-php/vhs/database/joins/JoinLeft.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinLeft extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateLeft($this); diff --git a/vhs/database/joins/JoinOuter.php b/packages/backend-php/vhs/database/joins/JoinOuter.php similarity index 93% rename from vhs/database/joins/JoinOuter.php rename to packages/backend-php/vhs/database/joins/JoinOuter.php index 50806db4..884242fe 100644 --- a/vhs/database/joins/JoinOuter.php +++ b/packages/backend-php/vhs/database/joins/JoinOuter.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinOuter extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateOuter($this); diff --git a/vhs/database/joins/JoinRight.php b/packages/backend-php/vhs/database/joins/JoinRight.php similarity index 93% rename from vhs/database/joins/JoinRight.php rename to packages/backend-php/vhs/database/joins/JoinRight.php index 5e2b3c7d..d9c9774c 100644 --- a/vhs/database/joins/JoinRight.php +++ b/packages/backend-php/vhs/database/joins/JoinRight.php @@ -9,6 +9,7 @@ namespace vhs\database\joins; +/** @typescript */ class JoinRight extends Join { public function generateJoin(IJoinGenerator $generator) { return $generator->generateRight($this); diff --git a/packages/backend-php/vhs/database/limits/ILimitGenerator.php b/packages/backend-php/vhs/database/limits/ILimitGenerator.php new file mode 100644 index 00000000..6b5c4b2f --- /dev/null +++ b/packages/backend-php/vhs/database/limits/ILimitGenerator.php @@ -0,0 +1,17 @@ +limit = $limit; + } + + /** + * Static Limit instantiator. + * + * @param int|null $limit + * + * @return Limit + */ + public static function Limit($limit) { + return new Limit($limit); + } + + /** + * generate. + * + * @param ILimitGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + return $this->generateLimit($generator); + } + + /** + * generateLimit. + * + * @param \vhs\database\limits\ILimitGenerator $generator + * + * @return mixed + */ + private function generateLimit(ILimitGenerator $generator) { + return $generator->generateLimit($this); + } +} diff --git a/packages/backend-php/vhs/database/offsets/IOffsetGenerator.php b/packages/backend-php/vhs/database/offsets/IOffsetGenerator.php new file mode 100644 index 00000000..0dadec83 --- /dev/null +++ b/packages/backend-php/vhs/database/offsets/IOffsetGenerator.php @@ -0,0 +1,17 @@ +offset = $offset; + } + + /** + * Offset. + * + * @param mixed $offset + * + * @return \vhs\database\offsets\Offset + */ + public static function Offset($offset) { + return new Offset($offset); + } + + /** + * generate. + * + * @param \vhs\database\IGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate($generator, $value = null) { + /** @var \vhs\database\offsets\IOffsetGenerator $generator */ + return $this->generateOffset($generator); + } + + /** + * generateOffset. + * + * @param \vhs\database\offsets\IOffsetGenerator $generator + * + * @return mixed + */ + private function generateOffset($generator) { + return $generator->generateOffset($this); + } +} diff --git a/packages/backend-php/vhs/database/orders/IOrderByGenerator.php b/packages/backend-php/vhs/database/orders/IOrderByGenerator.php new file mode 100644 index 00000000..e5886be9 --- /dev/null +++ b/packages/backend-php/vhs/database/orders/IOrderByGenerator.php @@ -0,0 +1,33 @@ +column = $column; + $this->orderBy = $orderBy; + } + + /** + * Ascending. + * + * @param Column $column + * @param OrderBy ...$orderBy + * + * @return OrderByAscending + */ + public static function Ascending(Column $column, OrderBy ...$orderBy) { + return new OrderByAscending($column, ...$orderBy); + } + + /** + * Descending. + * + * @param Column $column + * @param OrderBy ...$orderBy + * + * @return OrderByDescending + */ + public static function Descending(Column $column, OrderBy ...$orderBy) { + return new OrderByDescending($column, ...$orderBy); + } + + /** + * generateOrderBy. + * + * @param IOrderByGenerator $generator + * + * @return mixed + */ + abstract public function generateOrderBy(IOrderByGenerator $generator); + + /** + * generate. + * + * @param IOrderByGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + return $this->generateOrderBy($generator); + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + $this->column->__updateTable($table); + + foreach ($this->orderBy as $orderBy) { + $orderBy->__updateTable($table); + } + } +} diff --git a/packages/backend-php/vhs/database/orders/OrderByAscending.php b/packages/backend-php/vhs/database/orders/OrderByAscending.php new file mode 100644 index 00000000..92079dbb --- /dev/null +++ b/packages/backend-php/vhs/database/orders/OrderByAscending.php @@ -0,0 +1,24 @@ +generateAscending($this); + } +} diff --git a/packages/backend-php/vhs/database/orders/OrderByDescending.php b/packages/backend-php/vhs/database/orders/OrderByDescending.php new file mode 100644 index 00000000..b1c15484 --- /dev/null +++ b/packages/backend-php/vhs/database/orders/OrderByDescending.php @@ -0,0 +1,24 @@ +generateDescending($this); + } +} diff --git a/packages/backend-php/vhs/database/queries/IQueryGenerator.php b/packages/backend-php/vhs/database/queries/IQueryGenerator.php new file mode 100644 index 00000000..2603c098 --- /dev/null +++ b/packages/backend-php/vhs/database/queries/IQueryGenerator.php @@ -0,0 +1,60 @@ +table = $table; + $this->where = $where; + } + + /** + * Count. + * + * @param \vhs\database\Table $table + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param \vhs\database\limits\Limit|null $limit + * @param \vhs\database\offsets\Offset|null $offset + * + * @return \vhs\database\queries\QueryCount + */ + public static function Count(Table $table, ?Where $where = null, ?OrderBy $orderBy = null, ?Limit $limit = null, ?Offset $offset = null) { + return new QueryCount($table, $where, $orderBy, $limit, $offset); + } + + /** + * Delete. + * + * @param \vhs\database\Table $table + * @param \vhs\database\wheres\Where|null $where + * + * @return \vhs\database\queries\QueryDelete + */ + public static function Delete(Table $table, ?Where $where = null) { + return new QueryDelete($table, $where); + } + + /** + * Insert. + * + * @param \vhs\database\Table $table + * @param \vhs\database\Columns $columns + * @param mixed[] $values + * + * @return \vhs\database\queries\QueryInsert + */ + public static function Insert(Table $table, Columns $columns, array $values) { + return new QueryInsert($table, $columns, $values); + } + + /** + * Select. + * + * @param \vhs\database\Table $table + * @param \vhs\database\Columns|null $columns + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param \vhs\database\limits\Limit|null $limit + * @param \vhs\database\offsets\Offset|null $offset + * + * @return \vhs\database\queries\QuerySelect + */ + public static function Select( + Table $table, + ?Columns $columns = null, + ?Where $where = null, + ?OrderBy $orderBy = null, + ?Limit $limit = null, + ?Offset $offset = null + ) { + return new QuerySelect($table, $columns, $where, $orderBy, $limit, $offset); + } + + /** + * Update. + * + * @param \vhs\database\Table $table + * @param \vhs\database\Columns $columns + * @param \vhs\database\wheres\Where|null $where + * @param mixed[] $values + * + * @return \vhs\database\queries\QueryUpdate + */ + public static function Update(Table $table, Columns $columns, Where $where = null, array $values) { + return new QueryUpdate($table, $columns, $where, $values); + } + + /** + * generateQuery. + * + * @param \vhs\database\queries\IQueryGenerator $generator + * + * @return mixed + */ + abstract public function generateQuery(IQueryGenerator $generator); + + /** + * @param \vhs\database\IGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var IQueryGenerator $generator */ + return $this->generateQuery($generator); + } + + /** + * Join. + * + * @param \vhs\database\joins\Join ...$join + * + * @return self + */ + public function Join(Join ...$join) { + if (is_null($this->joins)) { + $this->joins = []; + } + + array_push($this->joins, ...$join); + + return $this; + } +} diff --git a/packages/backend-php/vhs/database/queries/QueryCount.php b/packages/backend-php/vhs/database/queries/QueryCount.php new file mode 100644 index 00000000..e1e793df --- /dev/null +++ b/packages/backend-php/vhs/database/queries/QueryCount.php @@ -0,0 +1,58 @@ +orderBy = $orderBy; + $this->limit = $limit; + $this->offset = $offset; + } + + /** + * generateQuery. + * + * @param \vhs\database\queries\IQueryGenerator $generator + * + * @return mixed + */ + public function generateQuery(IGenerator $generator) { + return $generator->generateSelectCount($this); + } +} diff --git a/packages/backend-php/vhs/database/queries/QueryDelete.php b/packages/backend-php/vhs/database/queries/QueryDelete.php new file mode 100644 index 00000000..aa1bb099 --- /dev/null +++ b/packages/backend-php/vhs/database/queries/QueryDelete.php @@ -0,0 +1,24 @@ +generateDelete($this); + } +} diff --git a/packages/backend-php/vhs/database/queries/QueryInsert.php b/packages/backend-php/vhs/database/queries/QueryInsert.php new file mode 100644 index 00000000..573c6bd5 --- /dev/null +++ b/packages/backend-php/vhs/database/queries/QueryInsert.php @@ -0,0 +1,39 @@ +columns = $columns; + $this->values = $values; + } + + public function generateQuery(IQueryGenerator $generator) { + return $generator->generateInsert($this); + } +} diff --git a/packages/backend-php/vhs/database/queries/QuerySelect.php b/packages/backend-php/vhs/database/queries/QuerySelect.php new file mode 100644 index 00000000..b7486d7e --- /dev/null +++ b/packages/backend-php/vhs/database/queries/QuerySelect.php @@ -0,0 +1,67 @@ +columns = $columns; + $this->orderBy = $orderBy; + $this->limit = $limit; + $this->offset = $offset; + } + + /** + * generateQuery. + * + * @param \vhs\database\queries\IQueryGenerator $generator + * + * @return mixed + */ + public function generateQuery(IQueryGenerator $generator) { + return $generator->generateSelect($this); + } +} diff --git a/packages/backend-php/vhs/database/queries/QueryUpdate.php b/packages/backend-php/vhs/database/queries/QueryUpdate.php new file mode 100644 index 00000000..18f179d4 --- /dev/null +++ b/packages/backend-php/vhs/database/queries/QueryUpdate.php @@ -0,0 +1,49 @@ +columns = $columns; + $this->values = $values; + } + + public function generateQuery(IQueryGenerator $generator) { + return $generator->generateUpdate($this); + } +} diff --git a/packages/backend-php/vhs/database/types/ITypeConverter.php b/packages/backend-php/vhs/database/types/ITypeConverter.php new file mode 100644 index 00000000..ac0a21bb --- /dev/null +++ b/packages/backend-php/vhs/database/types/ITypeConverter.php @@ -0,0 +1,95 @@ +nullable = $nullable; + $this->default = $default; + } + + /** + * Bool. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeBool + */ + public static function Bool($nullable = true, $default = null) { + return new TypeBool($nullable, $default); + } + + /** + * Date. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeDate + */ + public static function Date($nullable = true, $default = null) { + return new TypeDate($nullable, $default); + } + + /** + * DateTime. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeDateTime + */ + public static function DateTime($nullable = true, $default = null) { + return new TypeDateTime($nullable, $default); + } + + /** + * Enum. + * + * @param mixed $values + * + * @return TypeEnum + */ + public static function Enum(...$values) { + return new TypeEnum(...$values); + } + + /** + * Float. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeFloat + */ + public static function Float($nullable = true, $default = null) { + return new TypeFloat($nullable, $default); + } + + /** + * Int. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeInt + */ + public static function Int($nullable = true, $default = null) { + return new TypeInt($nullable, $default); + } + + /** + * String. + * + * @param bool $nullable + * @param mixed $default + * @param int $length + * + * @return TypeString + */ + public static function String($nullable = true, $default = null, $length = 255) { + return new TypeString($nullable, $default, $length); + } + + /** + * Text. + * + * @param bool $nullable + * @param mixed $default + * + * @return TypeText + */ + public static function Text($nullable = true, $default = null) { + return new TypeText($nullable, $default); + } + + /** + * convertType. + * + * @param ITypeConverter $converter + * @param mixed $value + * + * @return mixed + */ + abstract public function convertType(ITypeConverter $converter, $value = null); + + /** + * generateType. + * + * @param ITypeGenerator $generator + * @param mixed $value + * + * @return mixed + */ + abstract public function generateType(ITypeGenerator $generator, $value = null); + + /** + * @param IConverter $converter + * @param mixed $value + * + * @return mixed + */ + public function convert(IConverter $converter, $value = null) { + /** @var ITypeConverter $converter */ + return $this->convertType($converter, $value); + } + + /** + * @param IGenerator $generator + * @param mixed $value + * + * @return mixed + */ + public function generate(IGenerator $generator, $value = null) { + /** @var ITypeGenerator $generator */ + return $this->generateType($generator, $value); + } +} diff --git a/vhs/database/types/TypeBool.php b/packages/backend-php/vhs/database/types/TypeBool.php similarity index 79% rename from vhs/database/types/TypeBool.php rename to packages/backend-php/vhs/database/types/TypeBool.php index dd0075a2..b6ba5ba3 100644 --- a/vhs/database/types/TypeBool.php +++ b/packages/backend-php/vhs/database/types/TypeBool.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeBool extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertBool($this, $value); } diff --git a/vhs/database/types/TypeDate.php b/packages/backend-php/vhs/database/types/TypeDate.php similarity index 79% rename from vhs/database/types/TypeDate.php rename to packages/backend-php/vhs/database/types/TypeDate.php index 59ad4c72..4872d8a8 100644 --- a/vhs/database/types/TypeDate.php +++ b/packages/backend-php/vhs/database/types/TypeDate.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeDate extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertDate($this, $value); } diff --git a/vhs/database/types/TypeDateTime.php b/packages/backend-php/vhs/database/types/TypeDateTime.php similarity index 79% rename from vhs/database/types/TypeDateTime.php rename to packages/backend-php/vhs/database/types/TypeDateTime.php index fd273fe5..a7fb61b0 100644 --- a/vhs/database/types/TypeDateTime.php +++ b/packages/backend-php/vhs/database/types/TypeDateTime.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeDateTime extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertDateTime($this, $value); } diff --git a/packages/backend-php/vhs/database/types/TypeEnum.php b/packages/backend-php/vhs/database/types/TypeEnum.php new file mode 100644 index 00000000..d9e436cb --- /dev/null +++ b/packages/backend-php/vhs/database/types/TypeEnum.php @@ -0,0 +1,41 @@ +values = array_values($values); + } + + public function convertType(ITypeConverter $converter, $value = null) { + return $converter->convertEnum($this, $value); + } + + public function generateType(ITypeGenerator $generator, $value = null) { + return $generator->generateEnum($this, $value); + } +} diff --git a/vhs/database/types/TypeFloat.php b/packages/backend-php/vhs/database/types/TypeFloat.php similarity index 79% rename from vhs/database/types/TypeFloat.php rename to packages/backend-php/vhs/database/types/TypeFloat.php index 9fad7aa3..faf6a4ab 100644 --- a/vhs/database/types/TypeFloat.php +++ b/packages/backend-php/vhs/database/types/TypeFloat.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeFloat extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertFloat($this, $value); } diff --git a/vhs/database/types/TypeInt.php b/packages/backend-php/vhs/database/types/TypeInt.php similarity index 78% rename from vhs/database/types/TypeInt.php rename to packages/backend-php/vhs/database/types/TypeInt.php index 112fda35..3294fb1e 100644 --- a/vhs/database/types/TypeInt.php +++ b/packages/backend-php/vhs/database/types/TypeInt.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeInt extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertInt($this, $value); } diff --git a/packages/backend-php/vhs/database/types/TypeString.php b/packages/backend-php/vhs/database/types/TypeString.php new file mode 100644 index 00000000..86fd8594 --- /dev/null +++ b/packages/backend-php/vhs/database/types/TypeString.php @@ -0,0 +1,37 @@ +length = $length; + } + + public function convertType(ITypeConverter $converter, $value = null) { + return $converter->convertString($this, $value); + } + + public function generateType(ITypeGenerator $generator, $value = null) { + return $generator->generateString($this, $value); + } +} diff --git a/vhs/database/types/TypeText.php b/packages/backend-php/vhs/database/types/TypeText.php similarity index 79% rename from vhs/database/types/TypeText.php rename to packages/backend-php/vhs/database/types/TypeText.php index 28f41718..75a86b3b 100644 --- a/vhs/database/types/TypeText.php +++ b/packages/backend-php/vhs/database/types/TypeText.php @@ -9,8 +9,9 @@ namespace vhs\database\types; +/** @typescript */ class TypeText extends Type { - public function covertType(ITypeConverter $converter, $value = null) { + public function convertType(ITypeConverter $converter, $value = null) { return $converter->convertText($this, $value); } diff --git a/packages/backend-php/vhs/database/wheres/IWhereGenerator.php b/packages/backend-php/vhs/database/wheres/IWhereGenerator.php new file mode 100644 index 00000000..6b371667 --- /dev/null +++ b/packages/backend-php/vhs/database/wheres/IWhereGenerator.php @@ -0,0 +1,42 @@ +generateWhere($generator); + } + + abstract public function __toString(); +} diff --git a/packages/backend-php/vhs/database/wheres/WhereAnd.php b/packages/backend-php/vhs/database/wheres/WhereAnd.php new file mode 100644 index 00000000..2d8dc423 --- /dev/null +++ b/packages/backend-php/vhs/database/wheres/WhereAnd.php @@ -0,0 +1,70 @@ +wheres = $where; + } + + /** + * generateWhere. + * + * @param \vhs\database\wheres\IWhereGenerator $generator + * + * @return mixed + */ + public function generateWhere(IWhereGenerator $generator) { + return $generator->generateAnd($this); + } + + /** + * __toString. + * + * @return string + */ + public function __toString() { + $s = 'WhereAnd('; + + foreach ($this->wheres as $where) { + $s .= '' . $where; + } + + $s .= ')'; + + return $s; + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + foreach ($this->wheres as $where) { + $where->__updateTable($table); + } + } +} diff --git a/packages/backend-php/vhs/database/wheres/WhereComparator.php b/packages/backend-php/vhs/database/wheres/WhereComparator.php new file mode 100644 index 00000000..b6d3f890 --- /dev/null +++ b/packages/backend-php/vhs/database/wheres/WhereComparator.php @@ -0,0 +1,159 @@ +column = $column; + $this->value = $value; + $this->null_compare = $null_compare; + $this->equal = $equal; + $this->greater = $greater; + $this->lesser = $lesser; + $this->isArray = is_array($value); + $this->like = $like; + } + + /** + * generateWhere. + * + * @param \vhs\database\wheres\IWhereGenerator $generator + * + * @return callable + */ + public function generateWhere(IWhereGenerator $generator) { + return $generator->generateComparator($this); + } + + /** + * __toString. + * + * @return string + */ + public function __toString() { + $s = 'WhereComparator('; + + $s .= $this->column->name; + + if ($this->null_compare) { + $s .= ' null_compare '; + } + + if ($this->greater) { + $s .= ' greater '; + } + + if ($this->lesser) { + $s .= ' lesser '; + } + + if ($this->equal) { + $s .= ' equal '; + } + + if ($this->like) { + $s .= ' like '; + } + + if ($this->isArray) { + $s .= '(' . implode(', ', $this->value) . ')'; + } else { + $s .= '' . $this->value; + } + + $s .= ')'; + + return $s; + } + + /** + * __updateTable. + * + * @param \vhs\database\Table $table + * + * @return void + */ + public function __updateTable(Table &$table) { + $this->column->__updateTable($table); + } +} diff --git a/vhs/database/wheres/WhereOr.php b/packages/backend-php/vhs/database/wheres/WhereOr.php similarity index 86% rename from vhs/database/wheres/WhereOr.php rename to packages/backend-php/vhs/database/wheres/WhereOr.php index 8c949f09..b754b310 100644 --- a/vhs/database/wheres/WhereOr.php +++ b/packages/backend-php/vhs/database/wheres/WhereOr.php @@ -11,6 +11,7 @@ use vhs\database\Table; +/** @typescript */ class WhereOr extends Where { /** @var Where[] */ public $wheres = []; @@ -35,6 +36,13 @@ public function __toString() { return $s; } + /** + * __updateTable. + * + * @param Table $table + * + * @return void + */ public function __updateTable(Table &$table) { foreach ($this->wheres as $where) { $where->__updateTable($table); diff --git a/packages/backend-php/vhs/domain/Domain.php b/packages/backend-php/vhs/domain/Domain.php new file mode 100644 index 00000000..1d5c938b --- /dev/null +++ b/packages/backend-php/vhs/domain/Domain.php @@ -0,0 +1,1365 @@ +> + */ + private array $__collections; + + /** + * __dirtyChildren. + * + * @var bool + */ + private $__dirtyChildren = false; + + /** + * __parentRelationships. + * + * @var mixed + */ + private $__parentRelationships; + + /** + * __parentRelationshipsColumnMap. + * + * @var mixed + */ + private $__parentRelationshipsColumnMap; + + /** + * __construct. + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ + public function __construct() { + $schema = self::Schema(); + + if (is_null($schema)) { + throw new DomainException('Domain ' . get_called_class() . ' requires schema definition.'); + } + + $keys = []; + foreach ($schema->Columns()->all() as $col) { + array_push($keys, $col->getAbsoluteName()); + } + + $this->__cache = new DomainValueCache($keys); + + $pks = $schema->PrimaryKeys(); + $pkcols = []; + foreach ($pks as $pk) { + $pkcols[$pk->column->name] = $pk; + } + + foreach ($schema->Columns()->all() as $col) { + if (!array_key_exists($col->name, $pkcols)) { + $this->__cache->setValue($col->getAbsoluteName(), $col->type->default, true); + } + } + + $this->__collections = []; + $this->__parentRelationships = []; + $this->__parentRelationshipsColumnMap = []; + + $self = $this; + $dirtyChild = function () use ($self) { + $self->__dirtyChildren = true; + }; + + foreach (self::Relationships() as $as => $relationship) { + if (!is_null($relationship['JoinTable'])) { + $this->__collections[$as] = new SatelliteDomainCollection($this, $relationship['Domain'], $relationship['JoinTable']); + $this->__collections[$as]->onAdded($dirtyChild); + $this->__collections[$as]->onRemoved($dirtyChild); + } else { + $domain = $relationship['Domain']; + $myFks = self::Schema()->ForeignKeys(); + + $parentFk = null; + + /** @var ForeignKey $fk */ + foreach ($myFks as $fk) { + if ($fk->table === $domain::Schema()->Table()) { + $parentFk = $fk; + + break; + } + } + + if (!is_null($parentFk)) { + $this->__parentRelationships[$as] = []; + $this->__parentRelationships[$as]['Domain'] = $domain; + $this->__parentRelationships[$as]['Column'] = $parentFk->column; + $this->__parentRelationships[$as]['On'] = $parentFk->on; + $this->__parentRelationships[$as]['Object'] = null; + $this->__parentRelationshipsColumnMap[$parentFk->column->name] = $as; + } else { + // otherwise assume it must be a child relationship + $this->__collections[$as] = new ChildDomainCollection($this, $relationship['Domain']); + $this->__collections[$as]->onAdded($dirtyChild); + $this->__collections[$as]->onRemoved($dirtyChild); + } + } + } + } + + /** + * AccessDefinition. + * + * @return void + */ + public static function AccessDefinition() { + $checks = self::Schema()->Table()->checks; + + foreach ($checks as $check) { + $check->serialize(); + } + } + + /** + * Coerce filters value from string. + * + * @param string|\vhs\domain\Filter|null $filters + * + * @return void + */ + public static function coerceFilters(&$filters) { + if (is_string($filters)) { + // TODO total hack.. this is to support GET params + $filters = json_decode($filters); + } + } + + /** + * count. + * + * @param string|\vhs\domain\Filter|null $filters + * @param string[]|null $allowed_columns + * + * @return int + */ + public static function count($filters, ?array $allowed_columns = null) { + Domain::coerceFilters($filters); + + $where = self::constructFilterWhere($filters, $allowed_columns); + + return self::doCount($where); + } + + /** + * doCount. + * + * @param \vhs\database\wheres\Where|null $where + * + * @return int + */ + public static function doCount(?Where $where = null) { + $records = Database::count(Query::Count(self::Schema()->Table(), $where)); + + return (int) $records; + } + + /** + * @param array{id:int}|int $primaryKeyValues + * + * @return T|null + */ + public static function find($primaryKeyValues) { + /** @var class-string */ + $class = get_called_class(); + + $obj = new $class(); + + if (!$obj->hydrate($primaryKeyValues)) { + return null; + } + + return $obj; + } + + /** + * findAll. + * + * @return T[] + */ + public static function findAll() { + return self::hydrateMany(); + } + + /** + * onAnyBeforeChange. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyBeforeChange(callable $listener) { + self::staticOn('BeforeChange', $listener); + } + + /** + * onAnyBeforeCreate. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyBeforeCreate(callable $listener) { + self::staticOn('BeforeCreate', $listener); + } + + /** + * onAnyBeforeDelete. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyBeforeDelete(callable $listener) { + self::staticOn('BeforeDelete', $listener); + } + + /** + * onAnyBeforeSave. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyBeforeSave(callable $listener) { + self::staticOn('BeforeSave', $listener); + } + + /** + * onAnyBeforeUpdate. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyBeforeUpdate(callable $listener) { + self::staticOn('BeforeUpdate', $listener); + } + + /** + * onAnyChanged. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyChanged(callable $listener) { + self::staticOn('Changed', $listener); + } + + /** + * onAnyCreated. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyCreated(callable $listener) { + self::staticOn('Created', $listener); + } + + /** + * onAnyDeleted. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyDeleted(callable $listener) { + self::staticOn('Deleted', $listener); + } + + /** + * onAnySaved. + * + * @param callable $listener + * + * @return void + */ + public static function onAnySaved(callable $listener) { + self::staticOn('Saved', $listener); + } + + /** + * onAnyUpdated. + * + * @param callable $listener + * + * @return void + */ + public static function onAnyUpdated(callable $listener) { + self::staticOn('Updated', $listener); + } + + /** + * Returns a key value pair of data from this domain. + * + * @param int $page + * @param int $size + * @param string $columns + * @param string $order + * @param string|\vhs\domain\Filter|null $filters + * @param string[]|null $allowed_columns + * + * @return T[] + */ + public static function page($page, $size, $columns, $order, $filters, ?array $allowed_columns = null) { + Domain::coerceFilters($filters); + + $columnNames = explode(',', $columns); + $orders = explode(',', $order); + + if (is_array($allowed_columns) && !empty($allowed_columns)) { + $columnNames = array_intersect($allowed_columns, $columnNames); + } + + $cols = []; + $orderBys = []; + + foreach ($orders as $col) { + $isDesc = false; + if (strpos($col, ' desc')) { + $isDesc = true; + $col = str_replace(' desc', '', $col); + } + + if (self::Schema()->Columns()->contains($col)) { + array_push( + $orderBys, + $isDesc + ? OrderBy::Descending(self::Schema()->Columns()->getByName($col)) + : OrderBy::Ascending(self::Schema()->Columns()->getByName($col)) + ); + } + } + + /** @var OrderBy $orderBy */ + $orderBy = array_pop($orderBys); + $orderBy->orderBy = $orderBys; + + foreach ($columnNames as $col) { + if (self::Schema()->Columns()->contains($col)) { + array_push($cols, $col); + } + + if (array_key_exists($col, self::Relationships())) { + array_push($cols, $col); + } + } + + $where = self::constructFilterWhere($filters, $allowed_columns); + + $objects = self::where($where, $orderBy, $size, $page); + + /** @var T[] */ + $retval = []; + + foreach ($objects as $object) { + $val = []; + + foreach ($cols as $col) { + if (array_key_exists($col, self::Relationships())) { + $val[$col] = $object->$col->all(); + } else { + $val[$col] = $object->$col; + } + } + + array_push($retval, $val); + } + + return $retval; + } + + /** + * @param Schema|null $schema + * + * @return Schema + */ + public static function Schema(?Schema $schema = null) { + self::ensureDefined(); + + $class = get_called_class(); + + if (!is_null($schema)) { + self::$__definition[$class]['Schema'] = $schema; + } + + return self::$__definition[$class]['Schema']; + } + + /** + * @return string - Class name of type + */ + public static function Type() { + return get_called_class(); + } + + /** + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param mixed $limit + * @param mixed $offset + * + * @return T[] + */ + public static function where(?Where $where = null, ?OrderBy $orderBy = null, $limit = null, $offset = null) { + return self::hydrateMany($where, $orderBy, $limit, $offset); + } + + /** + * arbitraryFind. + * + * @param mixed $sql + * + * @return T[] + */ + protected static function arbitraryFind($sql) { + return self::arbitraryHydrate($sql); + } + + /** + * hydrateMany. + * + * @param \vhs\database\wheres\Where|null $where + * @param \vhs\database\orders\OrderBy|null $orderBy + * @param int $limit + * @param int $offset + * + * @return T[] + */ + protected static function hydrateMany(?Where $where = null, ?OrderBy $orderBy = null, $limit = null, $offset = null) { + $class = get_called_class(); + + $records = Database::select( + Query::Select( + self::Schema()->Table(), + self::Schema()->Columns(), + $where, + $orderBy, + !is_null($limit) ? Limit::Limit($limit) : null, + !is_null($offset) ? Offset::Offset($offset) : null + ) + ); + + $items = []; + + foreach ($records as $row) { + $obj = new $class(); + + $obj->setValues($row); + $obj->hydrateRelationships(); + + array_push($items, $obj); + } + + return $items; + } + + /** + * @param string $as + * @param string $domain + * @param \vhs\domain\Schema|null $joinTable + * + * @return void + */ + protected static function Relationship($as, $domain, ?Schema $joinTable = null) { + self::ensureDefined(); + + $class = get_called_class(); + + self::$__definition[$class]['Relationships'][$as] = []; + self::$__definition[$class]['Relationships'][$as]['Domain'] = $domain; + self::$__definition[$class]['Relationships'][$as]['JoinTable'] = $joinTable; + } + + /** + * Relationships. + * + * @return mixed + */ + protected static function Relationships() { + self::ensureDefined(); + + $class = get_called_class(); + + return self::$__definition[$class]['Relationships']; + } + + /** + * arbitrary hydrate. + * + * @param mixed $sql + * + * @return T[] + */ + private static function arbitraryHydrate($sql) { + /** @var class-string @class */ + $class = get_called_class(); + + $records = Database::arbitrary($sql); + + /** @var T[] */ + $items = []; + + foreach ($records as $row) { + $obj = new $class(); + + $obj->setValues($row); + $obj->hydrateRelationships(); + + array_push($items, $obj); + } + + return $items; + } + + /** + * Expects an object format like: + * Expression { + * left: Expression, + * operator: Operator, + * right: Expression, + * column: Column, + * value: Value + * } + * + * @param Columns $columns the filters that are allowed to be used in the filter + * @param mixed $filter + * + * @return void|Where|null + */ + private static function constructFilter(Columns $columns, $filter) { + if (is_object($filter)) { + if ($filter->operator != 'and' && $filter->operator != 'or') { + if (!$columns->contains($filter->column)) { + return null; + } + } + + switch ($filter->operator) { + case 'and': + return Where::_And(self::constructFilter($columns, $filter->left), self::constructFilter($columns, $filter->right)); + case 'or': + return Where::_Or(self::constructFilter($columns, $filter->left), self::constructFilter($columns, $filter->right)); + case '=': + return Where::Equal($columns->getByName($filter->column), $filter->value); + case '!=': + return Where::NotEqual($columns->getByName($filter->column), $filter->value); + case '>': + return Where::Greater($columns->getByName($filter->column), $filter->value); + case '<': + return Where::Lesser($columns->getByName($filter->column), $filter->value); + case '>=': + return Where::GreaterEqual($columns->getByName($filter->column), $filter->value); + case '<=': + return Where::LesserEqual($columns->getByName($filter->column), $filter->value); + case 'like': + return Where::Like($columns->getByName($filter->column), $filter->value); + case 'is null': + return Where::Null($columns->getByName($filter->column)); + case 'not null': + return Where::NotNull($columns->getByName($filter->column)); + default: + return null; + } + } + } + + /** + * Constructs the WHERE clause for a filter expression. + * + * @param mixed $filters + * @param string[]|null $allowed_columns either an array of strings containing the list of columns allowed in a filter expression or null which means al columns are allowed + * + * @return Where|null + */ + private static function constructFilterWhere($filters, ?array $allowed_columns = null) { + $actualColumns = new Columns(); + + if ($allowed_columns == null) { + // all table columns are allowed + $actualColumns = self::Schema()->Columns(); + } else { + // only some columns are allowed + foreach ($allowed_columns as $col) { + if (self::Schema()->Columns()->contains($col)) { + $actualColumns->add(self::Schema()->Columns()->getByName($col)); + } + } + } + + return self::constructFilter($actualColumns, $filters); + } + + /** + * ensureDefined. + * + * @return void + */ + private static function ensureDefined() { + $class = get_called_class(); + + if (!array_key_exists($class, self::$__definition)) { + self::$__definition[$class] = []; + self::$__definition[$class]['Schema'] = null; + self::$__definition[$class]['Relationships'] = []; + $class::Define(); + } + } + + /** + * @param ValidationResults $results + * + * @return bool|void + */ + abstract public function validate(ValidationResults &$results); + + /** + * delete. + * + * @return void + */ + public function delete() { + $this->raiseBeforeDelete(); + + Database::delete(Query::Delete(self::Schema()->Table(), $this->pkWhere())); + + $pks = self::Schema()->PrimaryKeys(); + foreach ($pks as $pk) { + $this->__set($pk->column->name, null); + } + + $this->raiseDeleted(); + } + + /** + * getInternalData. + * + * @return array + */ + public function getInternalData() { + $cols = self::Schema()->Columns()->all(); + $data = []; + foreach ($cols as $col) { + if ($col->serializable) { + $data[$col->name] = $this->__cache->getValue($col->getAbsoluteName()); + } + } + + foreach ($this->__collections as $relationship => $collection) { + $data[$relationship] = []; + + foreach ($collection->all() as $item) { + array_push($data[$relationship], $item->getInternalData()); + } + } + + foreach ($this->__parentRelationships as $relationship => $value) { + if (!is_null($value['Object'])) { + $data[$relationship] = $value['Object']->getInternalData(); + } else { + $data[$relationship] = null; + } + } + + return $data; + } + + /** + * jsonSerialize. + * + * @return mixed + */ + public function jsonSerialize(): mixed { + return $this->getInternalData(); + } + + /** + * onBeforeChange. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeChange(callable $listener) { + $this->on('BeforeChange', $listener); + } + + /** + * onBeforeCreate. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeCreate(callable $listener) { + $this->on('BeforeCreate', $listener); + } + + /** + * onBeforeDelete. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeDelete(callable $listener) { + $this->on('BeforeDelete', $listener); + } + + /** + * onBeforeSave. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeSave(callable $listener) { + $this->on('BeforeSave', $listener); + } + + /** + * onBeforeUpdate. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeUpdate(callable $listener) { + $this->on('BeforeUpdate', $listener); + } + + /** + * onChanged. + * + * @param callable $listener + * + * @return void + */ + public function onChanged(callable $listener) { + $this->on('Changed', $listener); + } + + /** + * onCreated. + * + * @param callable $listener + * + * @return void + */ + public function onCreated(callable $listener) { + $this->on('Created', $listener); + } + + /** + * onDeleted. + * + * @param callable $listener + * + * @return void + */ + public function onDeleted(callable $listener) { + $this->on('Deleted', $listener); + } + + /** + * onSaved. + * + * @param callable $listener + * + * @return void + */ + public function onSaved(callable $listener) { + $this->on('Saved', $listener); + } + + /** + * onUpdated. + * + * @param callable $listener + * + * @return void + */ + public function onUpdated(callable $listener) { + $this->on('Updated', $listener); + } + + /** + * @param ValidationResults|null $validationResults + * + * @throws \vhs\domain\exceptions\DomainException + * @throws \vhs\domain\validations\ValidationException + * + * @return bool + */ + public function save(&$validationResults = null) { + if (is_null($validationResults)) { + $vr = new ValidationResults(); + } else { + $vr = $validationResults; + } + + $this->validate($vr); + + if (!$vr->isSuccess()) { + if (isset($validationResults)) { + $validationResults = $vr; + + return false; + } else { + throw new ValidationException($vr); + } + } + + if (!$this->checkIsDirty()) { + return true; + } + + $this->raiseBeforeSave(); + + $isNew = $this->checkIsNew(); + + if ($isNew) { + $this->raiseBeforeCreate(); + + $pks = Database::insert(Query::Insert(self::Schema()->Table(), self::Schema()->Columns(), $this->getValues(true))); + + $this->setValues($this->extractPkValues($pks)); + } else { + $this->raiseBeforeUpdate(); + + Database::update(Query::Update(self::Schema()->Table(), self::Schema()->Columns(), $this->pkWhere(), $this->getValues(true))); + } + + foreach ($this->__collections as $collection) { + $collection->save(); + } + + foreach ($this->__parentRelationships as $as => $relationship) { + if (!is_null($this->__parentRelationships[$as]['Object'])) { + $this->__parentRelationships[$as]['Object']->save(); + } + } + + $this->__dirtyChildren = false; + + $this->hydrate(); + + if ($isNew) { + $this->raiseCreated(); + } else { + $this->raiseUpdated(); + } + + $this->raiseSaved(); + + return true; + } + + /** + * serialize. + * + * @return string + */ + public function serialize(): string { + return serialize($this->getInternalData()); + } + + /** + * unserialize. + * + * @param mixed $data + * + * @return void + */ + public function unserialize($data): void { + // TODO implement + } + + /** + * getValue. + * + * @param string $name + * + * @return mixed + */ + protected function getValue($name) { + if (self::Schema()->Columns()->contains($name)) { + return $this->$name; + } + + return null; + } + + /** + * getValues. + * + * @param mixed $excludePrimaryKeys + * + * @return mixed[] + */ + protected function getValues($excludePrimaryKeys = false) { + $data = []; + + $pkcols = []; + $pks = self::Schema()->PrimaryKeys(); + foreach ($pks as $pk) { + array_push($pkcols, $pk->column->name); + } + + $isPkCol = function ($name) use ($pkcols) { + foreach ($pkcols as $pkcol) { + if ($name === $pkcol) { + return true; + } + } + + return false; + }; + + foreach (self::Schema()->Columns()->all() as $col) { + if ($excludePrimaryKeys && $isPkCol($col->name)) { + continue; + } + + $data[$col->name] = $this->__cache->getValue($col->getAbsoluteName()); + } + + return $data; + } + + /** + * hydrate. + * + * @param mixed $pk + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return bool + */ + protected function hydrate($pk = null) { + $record = Database::select(Query::Select(self::Schema()->Table(), self::Schema()->Columns(), $this->pkWhere($pk))); + + if (sizeof($record) != 1) { + if (empty($record)) { + return false; + } else { + throw new DomainException('Primary Key based hydrate on {' . get_called_class() . '} returns more than one record.'); + } + } + + $this->setValues($record[0]); + + $this->hydrateRelationships(); + + return true; + } + + /** + * raiseBeforeChange. + * + * @param \vhs\database\Column ...$columns + * + * @return void + */ + protected function raiseBeforeChange(Column ...$columns) { + $this->raise('BeforeChange', ...$columns); + self::staticRaise('BeforeChange', $this, ...$columns); + } + + /** + * raiseBeforeCreate. + * + * @return void + */ + protected function raiseBeforeCreate() { + $this->raise('BeforeCreate'); + self::staticRaise('BeforeCreate', $this); + } + + /** + * raiseBeforeDelete. + * + * @return void + */ + protected function raiseBeforeDelete() { + $this->raise('BeforeDelete'); + self::staticRaise('BeforeDelete', $this); + } + + /** + * raiseBeforeSave. + * + * @return void + */ + protected function raiseBeforeSave() { + $this->raise('BeforeSave'); + self::staticRaise('BeforeSave', $this); + } + + /** + * raiseBeforeUpdate. + * + * @return void + */ + protected function raiseBeforeUpdate() { + $this->raise('BeforeUpdate'); + self::staticRaise('BeforeUpdate', $this); + } + + /** + * raiseChanged. + * + * @param \vhs\database\Column ...$columns + * + * @return void + */ + protected function raiseChanged(Column ...$columns) { + $this->raise('Changed', ...$columns); + self::staticRaise('Changed', $this, ...$columns); + } + + /** + * raiseCreated. + * + * @return void + */ + protected function raiseCreated() { + $this->raise('Created'); + self::staticRaise('Created', $this); + } + + /** + * raiseDeleted. + * + * @return void + */ + protected function raiseDeleted() { + $this->raise('Deleted'); + self::staticRaise('Deleted', $this); + } + + /** + * raiseSaved. + * + * @return void + */ + protected function raiseSaved() { + $this->raise('Saved'); + self::staticRaise('Saved', $this); + } + + /** + * raiseUpdated. + * + * @return void + */ + protected function raiseUpdated() { + $this->raise('Updated'); + self::staticRaise('Updated', $this); + } + + /** + * setValues. + * + * @param mixed $data + * + * @return void + */ + protected function setValues($data) { + $cols = []; + foreach (self::Schema()->Columns()->all() as $col) { + if (array_key_exists($col->name, $data)) { + array_push($cols, $col); + } + } + + $this->raiseBeforeChange(...$cols); + + foreach ($cols as $col) { + $this->__cache->setValue($col->getAbsoluteName(), $data[$col->name], true); + } + + $this->raiseChanged(...$cols); + } + + /** + * checkIsDirty. + * + * @return bool + */ + private function checkIsDirty() { + return $this->__cache->hasChanged() || $this->__dirtyChildren; + } + + /** + * checkIsNew. + * + * @return bool + */ + private function checkIsNew() { + $pks = self::Schema()->PrimaryKeys(); + + $isNew = false; + foreach ($pks as $pk) { + $isNew = $isNew || is_null($this->__get($pk->column->name)); + } + + return $isNew; + } + + /** + * extractPkValues. + * + * @param mixed $primaryKeyValues + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return string[] + */ + private function extractPkValues($primaryKeyValues = null) { + $pks = self::Schema()->PrimaryKeys(); + + if (count($pks) <= 0) { + throw new DomainException('Schema on domain must have Primary Keys'); + } + + $values = []; + + foreach ($pks as $pk) { + $value = $this->__get($pk->column->name); + + if (!is_null($primaryKeyValues)) { + if (is_array($primaryKeyValues)) { + $value = $primaryKeyValues[$pk->column->name]; + } else { + $value = $primaryKeyValues; + } + } + + $values[$pk->column->name] = $value; + } + + return $values; + } + + /** + * hydrateRelationships. + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ + private function hydrateRelationships() { + /** @var DomainCollection $collection */ + foreach ($this->__collections as $collection) { + $collection->hydrate(); + } + + foreach ($this->__parentRelationships as $as => $relationship) { + $on = $relationship['On']; + $domain = $relationship['Domain']; + $column = $relationship['Column']->name; + + $obj = $domain::where(Where::Equal($on, $this->$column)); + + if (count($obj) == 1) { + $this->__parentRelationships[$as]['Object'] = $obj[0]; + $self = $this; + $this->__parentRelationships[$as]['Object']->onChanged(function () use ($self) { + $self->__dirtyChildren = true; + }); + } elseif (count($obj) > 1) { + throw new DomainException("Parent relationship [{$as}] found more than one record"); + } else { + $this->__parentRelationships[$as]['Object'] = null; + } + } + } + + /** + * pkWhere. + * + * @param mixed $primaryKeyValues + * + * @return \vhs\database\wheres\Where|null + */ + private function pkWhere($primaryKeyValues = null) { + $values = $this->extractPkValues($primaryKeyValues); + + $wheres = []; + + if (count($values) > 0) { + foreach ($values as $key => $value) { + array_push($wheres, Where::Equal(self::Schema()->Column($key), $value)); + } + } + + if (count($wheres) > 1) { + return Where::_And(...$wheres); + } elseif (count($wheres) == 1) { + return $wheres[0]; + } + + return null; + } + + /** + * __get. + * + * @param string $name + * + * @return mixed + */ + public function __get($name) { + $internal = 0 === strpos($name, 'internal_'); + if ($internal) { + $name = substr($name, strlen('internal_')); + } + + if (!$internal && method_exists($this, $method = 'get_' . $name)) { + return $this->$method(); + } elseif (self::Schema()->Columns()->contains($name)) { + $col = self::Schema()->Columns()->getByName($name); + + return $this->__cache->getValue($col->getAbsoluteName()); + } elseif (array_key_exists($name, $this->__collections)) { + return $this->__collections[$name]; + } elseif (array_key_exists($name, $this->__parentRelationships)) { + return $this->__parentRelationships[$name]['Object']; + } + + return null; + } + + /** + * __serialize. + * + * @return array + */ + public function __serialize() { + return $this->getInternalData(); + } + + /** + * __set. + * + * @param mixed $name + * @param mixed $value + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ + public function __set($name, $value) { + $internal = 0 === strpos($name, 'internal_'); + if ($internal) { + $name = substr($name, strlen('internal_')); + } + + if (!$internal && method_exists($this, $method = 'set_' . $name)) { + $this->$method($value); + } elseif (self::Schema()->Columns()->contains($name)) { + $col = self::Schema()->Columns()->getByName($name); + $this->raiseBeforeChange($col); + $this->__cache->setValue($col->getAbsoluteName(), $value); + $this->raiseChanged($col); + } elseif (array_key_exists($name, $this->__collections)) { + throw new DomainException('Cannot directly set domain collection [' . get_called_class() . "->{$name}]"); + } elseif (array_key_exists($name, $this->__parentRelationships)) { + $childOnCol = $this->__parentRelationships[$name]['On']->name; + $this->__set($this->__parentRelationships[$name]['Column']->name, $value->$childOnCol); + $this->__parentRelationships[$name]['Object'] = $value; + } + } + + /** + * __toString. + * + * @return string + */ + public function __toString() { + // TODO if the schema has primary keys we could likely simplify and use those. Or even use a hash of the record data + $cols = self::Schema()->Columns()->all(); + $data = []; + foreach ($cols as $col) { + if ($col->serializable) { + $data[$col->name] = $this->__cache->getValue($col->getAbsoluteName()); + } + } + + $result = json_encode($data); + + if (!is_string($result)) { + throw new \Exception('Failed to convert to string'); + } + + return $result; + } + + /** + * __unserialize. + * + * @param mixed $data + * + * @return void + */ + public function __unserialize($data) { + // TODO implement + } +} diff --git a/packages/backend-php/vhs/domain/DomainValueCache.php b/packages/backend-php/vhs/domain/DomainValueCache.php new file mode 100644 index 00000000..5ac1b001 --- /dev/null +++ b/packages/backend-php/vhs/domain/DomainValueCache.php @@ -0,0 +1,91 @@ +keys = $keys; + $this->cache = array_fill_keys($this->keys, null); + } + + /** + * clear. + * + * @return void + */ + public function clear() { + unset($this->cache); + $this->cache = array_fill_keys($this->keys, null); + } + + /** + * getValue. + * + * @param mixed $name + * + * @return mixed + */ + public function getValue($name) { + return $this->cache[$name]; + } + + /** + * hasChanged. + * + * @return bool + */ + public function hasChanged() { + return $this->changed; + } + + /** + * setValue. + * + * @param mixed $name + * @param mixed $value + * @param mixed $silent + * + * @return void + */ + public function setValue($name, $value, $silent = false) { + $this->changed = $this->changed || !$silent; + + $this->cache[$name] = $value; + } +} diff --git a/packages/backend-php/vhs/domain/Filter.php b/packages/backend-php/vhs/domain/Filter.php new file mode 100644 index 00000000..c48e8f2e --- /dev/null +++ b/packages/backend-php/vhs/domain/Filter.php @@ -0,0 +1,198 @@ +left = $left; + $this->column = $column; + $this->operator = $operator; + $this->right = $right; + $this->value = $value; + } + + /** + * _And. + * + * @param mixed $left + * @param mixed $right + * + * @return \vhs\domain\Filter + */ + public static function _And($left, $right) { + return new Filter($left, null, 'and', $right, null); + } + + /** + * _Or. + * + * @param mixed $left + * @param mixed $right + * + * @return \vhs\domain\Filter + */ + public static function _Or($left, $right) { + return new Filter($left, null, 'or', $right, null); + } + + /** + * Equal. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Equal($column, $value) { + return new Filter(null, $column, '=', null, $value); + } + + /** + * Greater. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Greater($column, $value) { + return new Filter(null, $column, '>', null, $value); + } + + /** + * GreaterEqual. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function GreaterEqual($column, $value) { + return new Filter(null, $column, '>=', null, $value); + } + + /** + * IsNotNull. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function IsNotNull($column, $value) { + return new Filter(null, $column, 'not null', null, null); + } + + /** + * IsNull. + * + * @param mixed $column + * + * @return \vhs\domain\Filter + */ + public static function IsNull($column) { + return new Filter(null, $column, 'is null', null, null); + } + + /** + * Lesser. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Lesser($column, $value) { + return new Filter(null, $column, '<', null, $value); + } + + /** + * LesserEqual. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function LesserEqual($column, $value) { + return new Filter(null, $column, '<=', null, $value); + } + + /** + * Like. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function Like($column, $value) { + return new Filter(null, $column, 'like', null, $value); + } + + /** + * NotEqual. + * + * @param mixed $column + * @param mixed $value + * + * @return \vhs\domain\Filter + */ + public static function NotEqual($column, $value) { + return new Filter(null, $column, '!=', null, $value); + } +} diff --git a/packages/backend-php/vhs/domain/IDomain.php b/packages/backend-php/vhs/domain/IDomain.php new file mode 100644 index 00000000..a6933010 --- /dev/null +++ b/packages/backend-php/vhs/domain/IDomain.php @@ -0,0 +1,13 @@ +__ensureListeners($event); + + array_push($this->__listeners[$event], $listener); + } + + /** + * raise. + * + * @param mixed $event + * @param mixed ...$args + * + * @return void + */ + protected function raise($event, ...$args) { + $this->__ensureListeners($event); + + foreach ($this->__listeners[$event] as $listener) { + $listener($args); + } + } + + /** + * staticRaise. + * + * @param mixed $event + * @param mixed ...$args + * + * @return void + */ + protected function staticRaise($event, ...$args) { + self::__ensureStaticListeners($event); + + $class = get_called_class(); + + foreach (self::$__staticListeners[$class][$event] as $listener) { + $listener($args); + } + } + + /** + * __ensureListeners. + * + * @param mixed $event + * + * @return void + */ + private function __ensureListeners($event) { + if (is_null($this->__listeners)) { + $this->__listeners = []; + } + + if (!isset($this->__listeners[$event])) { + $this->__listeners[$event] = []; + } + } + + /** + * __ensureStaticListeners. + * + * @param mixed $event + * + * @return void + */ + private static function __ensureStaticListeners($event) { + if (is_null(self::$__staticListeners)) { + self::$__staticListeners = []; + } + + $class = get_called_class(); + + if (!isset(self::$__staticListeners[$class])) { + self::$__staticListeners[$class] = []; + } + + if (!isset(self::$__staticListeners[$class][$event])) { + self::$__staticListeners[$class][$event] = []; + } + } +} diff --git a/packages/backend-php/vhs/domain/Schema.php b/packages/backend-php/vhs/domain/Schema.php new file mode 100644 index 00000000..5e4de4c6 --- /dev/null +++ b/packages/backend-php/vhs/domain/Schema.php @@ -0,0 +1,139 @@ +internal_table = $this->init(); + } + + /** + * Table. + * + * @return \vhs\database\Table + */ + public static function &Table() { + return self::getInstance()->internal_table; + } + + /** + * Column. + * + * @param mixed $name + * + * @return \vhs\database\Column + */ + public static function Column($name) { + return self::Table()->columns->$name; + } + + /** + * @return Columns + */ + /** + * Columns. + * + * @return \vhs\database\Columns + */ + public static function Columns() { + return self::Table()->columns; + } + + /** + * Constraints. + * + * @return \vhs\database\constraints\Constraint[] + */ + public static function Constraints() { + return self::Table()->constraints; + } + + /** + * ForeignKeys. + * + * @return \vhs\database\constraints\ForeignKey[] + */ + public static function ForeignKeys() { + $fks = []; + + foreach (self::Table()->constraints as $constraint) { + if ($constraint instanceof ForeignKey) { + array_push($fks, $constraint); + } + } + + return $fks; + } + + /** + * PrimaryKeys. + * + * @return \vhs\database\constraints\PrimaryKey[] + */ + public static function PrimaryKeys() { + $pks = []; + foreach (self::Table()->constraints as $constraint) { + if ($constraint instanceof PrimaryKey) { + array_push($pks, $constraint); + } + } + + return $pks; + } + + /** + * Type. + * + * @return \vhs\domain\Schema + */ + final public static function Type() { + return self::getInstance(); + } + + /** + * getInstance. + * + * @return \vhs\domain\Schema + */ + private static function getInstance() { + static $aoInstance = []; + + $class = get_called_class(); + + if (!isset($aoInstance[$class])) { + $aoInstance[$class] = new $class(); + } + + return $aoInstance[$class]; + } + + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + } +} diff --git a/vhs/domain/collections/ChildDomainCollection.php b/packages/backend-php/vhs/domain/collections/ChildDomainCollection.php similarity index 76% rename from vhs/domain/collections/ChildDomainCollection.php rename to packages/backend-php/vhs/domain/collections/ChildDomainCollection.php index a528c846..56be63c4 100644 --- a/vhs/domain/collections/ChildDomainCollection.php +++ b/packages/backend-php/vhs/domain/collections/ChildDomainCollection.php @@ -11,22 +11,61 @@ use vhs\database\Column; use vhs\database\constraints\ForeignKey; -use vhs\database\constraints\PrimaryKey; use vhs\database\wheres\Where; use vhs\domain\Domain; use vhs\domain\exceptions\DomainException; +/** + * @template T of Domain + * + * @extends DomainCollection + * + * @typescript + */ class ChildDomainCollection extends DomainCollection { - /** @var Column $childColumn */ + /** + * childColumn. + * + * @var \vhs\database\Column + */ private $childColumn; - /** @var PrimaryKey $childKey */ + + /** + * childKey. + * + * @var \vhs\database\constraints\PrimaryKey + */ private $childKey; + + /** + * childType. + * + * @var mixed + */ private $childType; - /** @var Domain $parent */ + + /** + * parent. + * + * @var \vhs\domain\Domain + */ private $parent; - /** @var Column $parentColumn */ + + /** + * parentColumn. + * + * @var Column $parentColumn + */ private $parentColumn; + /** + * __construct. + * + * @param \vhs\domain\Domain $parent + * @param mixed $childType + * + * @throws \vhs\domain\exceptions\DomainException + */ public function __construct(Domain $parent, $childType) { $this->parent = $parent; $this->childType = $childType; @@ -55,11 +94,18 @@ public function __construct(Domain $parent, $childType) { throw new DomainException('Child relationship incomplete - missing referenced child and/or parent column on joined tables'); } - //TODO something with before deletes - maybe not because this is a direct relationship + // TODO something with before deletes - maybe not because this is a direct relationship $this->clear(); } + /** + * add. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function add(Domain $item) { $this->raiseBeforeAdd(); if ($this->contains($item)) { @@ -80,6 +126,11 @@ public function add(Domain $item) { $this->raiseAdded(); } + /** + * all. + * + * @return \vhs\domain\Domain[] + */ public function all() { $childPkName = $this->childKey->column->name; @@ -92,22 +143,49 @@ public function all() { return $all; } + /** + * clear. + * + * @return void + */ public function clear() { $this->__existing = []; $this->__new = []; $this->__removed = []; } + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ public function compare(Domain $a, Domain $b) { $childPkName = $this->childKey->column->name; + return $a->$childPkName === $b->$childPkName; } + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ public function contains(Domain $item) { $childPkName = $this->childKey->column->name; + return $this->containsKey($item->$childPkName); } + /** + * hydrate. + * + * @return void + */ public function hydrate() { $this->clear(); @@ -122,6 +200,13 @@ public function hydrate() { } } + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function remove(Domain $item) { $this->raiseBeforeRemove(); if ($this->contains($item)) { @@ -140,6 +225,11 @@ public function remove(Domain $item) { } } + /** + * save. + * + * @return void + */ public function save() { $this->raiseBeforeSave(); diff --git a/packages/backend-php/vhs/domain/collections/DomainCollection.php b/packages/backend-php/vhs/domain/collections/DomainCollection.php new file mode 100644 index 00000000..7ff4a5cb --- /dev/null +++ b/packages/backend-php/vhs/domain/collections/DomainCollection.php @@ -0,0 +1,243 @@ + $item + * + * @return void + */ + abstract public function add(Domain $item); + + /** + * all. + * + * @return \vhs\domain\Domain[] + */ + abstract public function all(); + + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ + abstract public function compare(Domain $a, Domain $b); + + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ + abstract public function contains(Domain $item); + + /** + * hydrate. + * + * @return void + */ + abstract public function hydrate(); + + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ + abstract public function remove(Domain $item); + + /** + * save. + * + * @return void + */ + abstract public function save(); + + /** + * clear. + * + * @return void + */ + public function clear() { + $this->__existing = []; + $this->__new = []; + $this->__removed = []; + } + + /** + * containsKey. + * + * @param string $key + * + * @return bool + */ + public function containsKey($key) { + return array_key_exists($key, $this->all()); + } + + /** + * onAdded. + * + * @param callable $listener + * + * @return void + */ + public function onAdded(callable $listener) { + $this->on('Added', $listener); + } + + /** + * onBeforeAdd. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeAdd(callable $listener) { + $this->on('BeforeAdd', $listener); + } + + /** + * onBeforeRemove. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeRemove(callable $listener) { + $this->on('BeforeRemove', $listener); + } + + /** + * onBeforeSave. + * + * @param callable $listener + * + * @return void + */ + public function onBeforeSave(callable $listener) { + $this->on('BeforeSave', $listener); + } + + /** + * onRemoved. + * + * @param callable $listener + * + * @return void + */ + public function onRemoved(callable $listener) { + $this->on('Removed', $listener); + } + + /** + * onSaved. + * + * @param callable $listener + * + * @return void + */ + public function onSaved(callable $listener) { + $this->on('Saved', $listener); + } + + /** + * raiseAdded. + * + * @return void + */ + protected function raiseAdded() { + $this->raise('Added'); + } + + /** + * raiseBeforeAdd. + * + * @return void + */ + protected function raiseBeforeAdd() { + $this->raise('BeforeUpdate'); + } + + /** + * raiseBeforeRemove. + * + * @return void + */ + protected function raiseBeforeRemove() { + $this->raise('BeforeDelete'); + } + + /** + * raiseBeforeSave. + * + * @return void + */ + protected function raiseBeforeSave() { + $this->raise('BeforeSave'); + } + + /** + * raiseRemoved. + * + * @return void + */ + protected function raiseRemoved() { + $this->raise('Removed'); + } + + /** + * raiseSaved. + * + * @return void + */ + protected function raiseSaved() { + $this->raise('Saved'); + } +} diff --git a/packages/backend-php/vhs/domain/collections/ParentDomainCollection.php b/packages/backend-php/vhs/domain/collections/ParentDomainCollection.php new file mode 100644 index 00000000..a06309bc --- /dev/null +++ b/packages/backend-php/vhs/domain/collections/ParentDomainCollection.php @@ -0,0 +1,117 @@ + + * + * @typescript + */ +class ParentDomainCollection extends DomainCollection { + /** + * add. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ + public function add(Domain $item) { + // TODO: Implement add() method. + $this->raiseBeforeAdd(); + $this->raiseAdded(); + } + + /** + * all. + * + * @return \vhs\domain\Domain[] + */ + public function all() { + // TODO: Implement all() method. + return []; + } + + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ + public function compare(Domain $a, Domain $b) { + // TODO: Implement compare() method. + + return false; + } + + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ + public function contains(Domain $item) { + // TODO: Implement contains() method. + return false; + } + + /** + * containsKey. + * + * @param int $key + * + * @return bool + * + * @phpstan-ignore method.childParameterType + */ + public function containsKey($key) { + // TODO: Implement containsKey() method. + return false; + } + + /** + * hydrate. + * + * @return void + */ + public function hydrate() { + // TODO: Implement hydrate() method. + } + + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ + public function remove(Domain $item) { + // TODO: Implement remove() method. + $this->raiseBeforeRemove(); + $this->raiseRemoved(); + } + + /** + * save. + * + * @return void + */ + public function save() { + // TODO: Implement save() method. + $this->raiseBeforeSave(); + $this->raiseSaved(); + } +} diff --git a/vhs/domain/collections/SatelliteDomainCollection.php b/packages/backend-php/vhs/domain/collections/SatelliteDomainCollection.php similarity index 77% rename from vhs/domain/collections/SatelliteDomainCollection.php rename to packages/backend-php/vhs/domain/collections/SatelliteDomainCollection.php index fb083386..cb635383 100644 --- a/vhs/domain/collections/SatelliteDomainCollection.php +++ b/packages/backend-php/vhs/domain/collections/SatelliteDomainCollection.php @@ -10,7 +10,6 @@ namespace vhs\domain\collections; use vhs\database\Columns; -use vhs\database\constraints\ForeignKey; use vhs\database\Database; use vhs\database\queries\Query; use vhs\database\wheres\Where; @@ -18,15 +17,58 @@ use vhs\domain\exceptions\DomainException; use vhs\domain\Schema; +/** + * @template T of Domain + * + * @extends DomainCollection + * + * @typescript + */ class SatelliteDomainCollection extends DomainCollection { - /** @var ForeignKey */ + /** + * childKey. + * + * @var \vhs\database\constraints\ForeignKey + */ private $childKey; + + /** + * childType. + * + * @var mixed + */ private $childType; + + /** + * joinTable. + * + * @var \vhs\domain\Schema + */ private $joinTable; + + /** + * parent. + * + * @var \vhs\domain\Domain + */ private $parent; - /** @var ForeignKey */ + + /** + * parentKey. + * + * @var \vhs\database\constraints\ForeignKey + */ private $parentKey; + /** + * __construct. + * + * @param \vhs\domain\Domain $parent + * @param mixed $childType + * @param \vhs\domain\Schema $joinTable + * + * @throws \vhs\domain\exceptions\DomainException + */ public function __construct(Domain $parent, $childType, Schema $joinTable) { $this->parent = $parent; $this->childType = $childType; @@ -59,6 +101,15 @@ public function __construct(Domain $parent, $childType, Schema $joinTable) { $this->clear(); } + /** + * add. + * + * @param \vhs\domain\Domain $item + * + * @throws \vhs\domain\exceptions\DomainException + * + * @return void + */ public function add(Domain $item) { $this->raiseBeforeAdd(); @@ -76,6 +127,11 @@ public function add(Domain $item) { $this->raiseAdded(); } + /** + * all. + * + * @return \vhs\domain\Domain[] + */ public function all() { $childOnCol = $this->childKey->on->name; @@ -88,16 +144,38 @@ public function all() { return $all; } + /** + * compare. + * + * @param \vhs\domain\Domain $a + * @param \vhs\domain\Domain $b + * + * @return bool + */ public function compare(Domain $a, Domain $b) { $childOnCol = $this->childKey->on->name; + return $a->$childOnCol === $b->$childOnCol; } + /** + * contains. + * + * @param \vhs\domain\Domain $item + * + * @return bool + */ public function contains(Domain $item) { $childOnCol = $this->childKey->on->name; + return $this->containsKey($item->$childOnCol); } + /** + * hydrate. + * + * @return void + */ public function hydrate() { $this->clear(); @@ -120,12 +198,19 @@ public function hydrate() { array_push($childIds, $row[$this->childKey->column->name]); } + /** @var Domain $childType */ $childType = $this->childType; - /** @var Domain $childType */ $this->__existing = $childType::where(Where::In($this->childKey->on, $childIds)); } + /** + * remove. + * + * @param \vhs\domain\Domain $item + * + * @return void + */ public function remove(Domain $item) { $this->raiseBeforeRemove(); if ($this->contains($item)) { @@ -140,6 +225,11 @@ public function remove(Domain $item) { } } + /** + * save. + * + * @return void + */ public function save() { $this->raiseBeforeSave(); diff --git a/vhs/domain/exceptions/DomainException.php b/packages/backend-php/vhs/domain/exceptions/DomainException.php similarity index 90% rename from vhs/domain/exceptions/DomainException.php rename to packages/backend-php/vhs/domain/exceptions/DomainException.php index ca00380d..32516a67 100644 --- a/vhs/domain/exceptions/DomainException.php +++ b/packages/backend-php/vhs/domain/exceptions/DomainException.php @@ -9,5 +9,6 @@ namespace vhs\domain\exceptions; +/** @typescript */ class DomainException extends \Exception { } diff --git a/vhs/domain/exceptions/InvalidColumnDefinitionException.php b/packages/backend-php/vhs/domain/exceptions/InvalidColumnDefinitionException.php similarity index 91% rename from vhs/domain/exceptions/InvalidColumnDefinitionException.php rename to packages/backend-php/vhs/domain/exceptions/InvalidColumnDefinitionException.php index a3b55e26..5118b71a 100644 --- a/vhs/domain/exceptions/InvalidColumnDefinitionException.php +++ b/packages/backend-php/vhs/domain/exceptions/InvalidColumnDefinitionException.php @@ -9,5 +9,6 @@ namespace vhs\domain\exceptions; +/** @typescript */ class InvalidColumnDefinitionException extends DomainException { } diff --git a/packages/backend-php/vhs/domain/validations/ValidationException.php b/packages/backend-php/vhs/domain/validations/ValidationException.php new file mode 100644 index 00000000..7d8d5e61 --- /dev/null +++ b/packages/backend-php/vhs/domain/validations/ValidationException.php @@ -0,0 +1,48 @@ +results = $results; + + $message = 'Validation failed:'; + + foreach ($this->results->getFailures() as $failure) { + $message .= "\t\n" . $failure->getMessage(); + } + + parent::__construct($message); + } + + /** + * getResults. + * + * @return ValidationResults + */ + public function getResults() { + return $this->results; + } +} diff --git a/packages/backend-php/vhs/domain/validations/ValidationFailure.php b/packages/backend-php/vhs/domain/validations/ValidationFailure.php new file mode 100644 index 00000000..0c81ade1 --- /dev/null +++ b/packages/backend-php/vhs/domain/validations/ValidationFailure.php @@ -0,0 +1,40 @@ +message = $message; + } + + /** + * getMessage. + * + * @return string + */ + public function getMessage() { + return $this->message; + } +} diff --git a/packages/backend-php/vhs/domain/validations/ValidationResults.php b/packages/backend-php/vhs/domain/validations/ValidationResults.php new file mode 100644 index 00000000..9b4d28da --- /dev/null +++ b/packages/backend-php/vhs/domain/validations/ValidationResults.php @@ -0,0 +1,49 @@ +failures, $failure); + } + + /** + * getFailures. + * + * @return \vhs\domain\validations\ValidationFailure[] + */ + public function getFailures() { + return $this->failures; + } + + /** + * isSuccess. + * + * @return bool + */ + public function isSuccess() { + return sizeof($this->failures) == 0; + } +} diff --git a/packages/backend-php/vhs/exceptions/HttpException.php b/packages/backend-php/vhs/exceptions/HttpException.php new file mode 100644 index 00000000..c304be2d --- /dev/null +++ b/packages/backend-php/vhs/exceptions/HttpException.php @@ -0,0 +1,25 @@ +value, $previous); + } +} diff --git a/packages/backend-php/vhs/gateways/Engine.php b/packages/backend-php/vhs/gateways/Engine.php new file mode 100644 index 00000000..df08e20f --- /dev/null +++ b/packages/backend-php/vhs/gateways/Engine.php @@ -0,0 +1,175 @@ +>> + */ + private $gatewayInstances = []; + + /** + * gatewaysPrefix. + * + * @var string + */ + private $gatewaysPrefix = 'vhs\gateways'; + + protected function __construct() { + parent::__construct(); + + $this->discover(); + } + + /** + * Get the default gateway for a particular category and type. + * + * E.g. \vhs\gateways\Engine::getInstance()->getDefaultGateway('messages', 'email') will get the default email gateway. + * + * @param string $gatewayCategory + * @param string $gatewayType + * + * @return object + */ + public function getDefaultGateway($gatewayCategory, $gatewayType): object { + if (!isset($this->gatewayInstances[$gatewayCategory][$gatewayType]['default'])) { + throw new \Exception(sprintf('No default gateway found for %s - %s', $gatewayCategory, $gatewayType)); + } + + return $this->getNamedGateway($gatewayCategory, $gatewayType, $this->gatewayInstances[$gatewayCategory][$gatewayType]['default']); + } + + /** + * Get a named gateway implementation for a particular category and type. + * + * E.g. \vhs\gateways\Engine::getInstance()->getDefaultGateway('messages', 'email', 'AWSSESClient') will get the AWSSESClient. + * + * @param string $gatewayCategory + * @param string $gatewayType + * @param string $className + * + * @return object + */ + public function getNamedGateway($gatewayCategory, $gatewayType, $className): object { + if (!isset($this->gatewayInstances[$gatewayCategory][$gatewayType][$className])) { + throw new \Exception(sprintf('No named gateway found for %s/%s/%s', $gatewayCategory, $gatewayType, $className)); + } + + return $this->gatewayInstances[$gatewayCategory][$gatewayType][$className]; + } + + /** + * Set default gateway implementation for a particular category and type. + * + * I.e. \vhs\gateways\Engine::getInstance()->setDefaultGateway('messages', 'email', 'AWSSESClient') will set AWSSESClient as the default email gateway. + * + * @param string $gatewayCategory + * @param string $gatewayType + * @param string $className + * + * @return void + */ + public function setDefaultGateway($gatewayCategory, $gatewayType, $className): void { + if (!isset($this->gatewayInstances[$gatewayCategory][$gatewayType][$className])) { + throw new \Exception(sprintf('No named gateway found for %s/%s/%s', $gatewayCategory, $gatewayType, $className)); + } + + $this->gatewayInstances[$gatewayCategory][$gatewayType]['default'] = $className; + } + + /** + * Magic auto-discovery and registration. + * + * @return void + */ + protected function discover(): void { + $gatewayFiles = $this->scanAllDir(__DIR__); + + foreach ($gatewayFiles as $gatewayFile) { + $gatewayDefinition = explode(DIRECTORY_SEPARATOR, str_replace('.php', '', $gatewayFile)); + + $this->register($gatewayDefinition[0], $gatewayDefinition[1], $gatewayDefinition[2]); + } + } + + /** + * Automagically register a class instance of a gateway. + * + * @param mixed $gatewayCategory The top-level category. E.g. 'messages' + * @param mixed $gatewayType The sub-level type. E.g. 'email' + * @param mixed $className The class name of the actual implementation. E.g. 'AWSSESClient' + * @param bool $autoDefault Whether to automatically set the class as the default handler for that category/type. Defaults to true. + * + * @return void + */ + protected function register($gatewayCategory, $gatewayType, $className, $autoDefault = true): void { + $interfacePath = sprintf('%s\interfaces\I%s%sGateway', $this->gatewaysPrefix, ucfirst($gatewayCategory), ucfirst($gatewayType)); + $classPath = sprintf('%s\%s\%s\%s', $this->gatewaysPrefix, $gatewayCategory, $gatewayType, $className); + + $gatewayClass = new \ReflectionClass($classPath); + + if ( + !in_array($interfacePath, $gatewayClass->getInterfaceNames()) && + !in_array(sprintf('%s\interfaces\IGateway', $this->gatewaysPrefix), $gatewayClass->getInterfaceNames()) + ) { + throw new \Exception( + sprintf( + 'Gateway (%s) does not implement required interface %s. Found: [%s]', + $classPath, + $interfacePath, + implode(',', $gatewayClass->getInterfaceNames()) + ) + ); + } + + if ($gatewayClass->getParentClass()->getName() !== 'vhs\Loggington') { + throw new \Exception( + sprintf('Gateway (%s) does not extend vhs\Loggington. Found: [%s]', $classPath, $gatewayClass->getParentClass()->getName()) + ); + } + + $this->gatewayInstances[$gatewayCategory][$gatewayType][$className] = call_user_func(sprintf('%s::getInstance', $classPath)); + + $this->gatewayInstances[$gatewayCategory][$gatewayType][$className]->setLogger($this->logger); + + if ($autoDefault && !isset($this->gatewayInstances[$gatewayCategory][$gatewayType]['default'])) { + $this->gatewayInstances[$gatewayCategory][$gatewayType]['default'] = $className; + } + } + + /** + * scanAllDir. + * + * @param string $scanDir + * + * @return string[] + */ + private function scanAllDir($scanDir) { + $result = []; + + foreach (scandir($scanDir) as $fileName) { + if (!preg_match('/^(\.\.?|interfaces|Engine.php)/', $fileName)) { + $filePath = $scanDir . DIRECTORY_SEPARATOR . $fileName; + + if (is_dir($filePath)) { + foreach ($this->scanAllDir($filePath) as $childFilename) { + $result[] = $fileName . DIRECTORY_SEPARATOR . $childFilename; + } + } else { + $result[] = $fileName; + } + } + } + + return $result; + } +} diff --git a/packages/backend-php/vhs/gateways/interfaces/IGateway.php b/packages/backend-php/vhs/gateways/interfaces/IGateway.php new file mode 100644 index 00000000..b982e572 --- /dev/null +++ b/packages/backend-php/vhs/gateways/interfaces/IGateway.php @@ -0,0 +1,7 @@ +client = new SesClient([ + 'region' => AWS_SES_REGION, + 'credentials' => [ + 'key' => AWS_SES_CLIENT_ID, + 'secret' => AWS_SES_SECRET + ] + ]); + } + + public function health(): bool { + return true; + } + + /** + * Send a rich (text+html) email. + * + * @param string|string[] $recipients Recipient address or addresses array + * @param string $subject Email subject + * @param mixed $textContent text content + * @param mixed $htmlContent html content + * + * @return bool + */ + public function sendRichEmail(string|array $recipients, string $subject, $textContent, $htmlContent): bool { + try { + $this->client->sendEmail([ + 'Source' => NOMOS_FROM_EMAIL, + 'Destination' => [ + 'ToAddresses' => is_array($recipients) ? $recipients : [$recipients] + ], + 'Message' => [ + 'Subject' => [ + // Data is required + 'Data' => $subject + ], + // Body is required + 'Body' => [ + 'Text' => [ + // Data is required + 'Data' => $textContent + ], + 'Html' => [ + // Data is required + 'Data' => $htmlContent + ] + ] + ] + ]); + + return true; + } catch (\Exception $e) { + $this->logger->log('An unknown error occured while trying to sendRichEmail: ' . $e->getMessage()); + + return false; + } + } + + /** + * Send a simple (plain text) email. + * + * @param string|string[] $recipients Recipient address or addresses array + * @param string $subject Email subject + * @param mixed $textContent text content + * + * @return bool + */ + public function sendSimpleEmail(string|array $recipients, string $subject, $textContent): bool { + try { + $this->client->sendEmail([ + 'Source' => NOMOS_FROM_EMAIL, + 'Destination' => [ + 'ToAddresses' => is_array($recipients) ? $recipients : [$recipients] + ], + 'Message' => [ + 'Subject' => [ + // Data is required + 'Data' => $subject + ], + // Body is required + 'Body' => [ + 'Text' => [ + // Data is required + 'Data' => $textContent + ] + ] + ] + ]); + + return true; + } catch (\Exception $e) { + $this->logger->log('An unknown error occured while trying to sendSimpleEmail: ' . $e->getMessage()); + + return false; + } + } +} diff --git a/packages/backend-php/vhs/loggers/ConsoleLogger.php b/packages/backend-php/vhs/loggers/ConsoleLogger.php new file mode 100644 index 00000000..eff28922 --- /dev/null +++ b/packages/backend-php/vhs/loggers/ConsoleLogger.php @@ -0,0 +1,26 @@ +filename = $filename; + } + + /** + * log. + * + * @param string $message + * + * @return void + */ + public function log($message) { + $this->ensureFile(); + if (is_resource($this->file)) { + fwrite($this->file, '[' . date('Y-m-d H:i:s') . '] ' . INSTANCE_ID . ' ' . CurrentUser::getPrincipal() . ' ' . $message . PHP_EOL); + } + } + + /** + * ensureFile. + * + * @return void + */ + private function ensureFile() { + if (!isset($this->file)) { + $this->file = fopen($this->filename, 'a'); + + if (!is_resource($this->file)) { + throw new \Exception('Failed to open log file'); + } + } + } +} diff --git a/packages/backend-php/vhs/loggers/SilentLogger.php b/packages/backend-php/vhs/loggers/SilentLogger.php new file mode 100644 index 00000000..db7623e5 --- /dev/null +++ b/packages/backend-php/vhs/loggers/SilentLogger.php @@ -0,0 +1,26 @@ +history = []; + } + + /** + * fullText. + * + * @return string + */ + public function fullText() { + return implode("\n", $this->history); + } + + /** + * log. + * + * @param mixed $message + * + * @return void + */ + public function log($message) { + array_push($this->history, $message); + } +} diff --git a/packages/backend-php/vhs/messaging/ConnectionInfo.php b/packages/backend-php/vhs/messaging/ConnectionInfo.php new file mode 100644 index 00000000..c4e4341e --- /dev/null +++ b/packages/backend-php/vhs/messaging/ConnectionInfo.php @@ -0,0 +1,29 @@ + + */ + abstract public function getDetails(); + + /** + * __toString. + * + * @return string + */ + public function __toString() { + return var_export($this->getDetails(), true); + } +} diff --git a/packages/backend-php/vhs/messaging/Engine.php b/packages/backend-php/vhs/messaging/Engine.php new file mode 100644 index 00000000..016b57c5 --- /dev/null +++ b/packages/backend-php/vhs/messaging/Engine.php @@ -0,0 +1,47 @@ +setLoggerInternal(new SilentLogger()); + $this->setRethrowInternal(true); + } + + /** + * __destruct. + * + * @return void + */ + public function __destruct() { + $this->engine->disconnect(); + } + + /** + * consume. + * + * @param mixed $channel + * @param mixed $queue + * @param callable $callback + * + * @return void + */ + public static function consume($channel, $queue, callable $callback) { + $mq = self::getInstance(); + + $mq->invokeEngine(function () use ($mq, $channel, $queue, $callback): string { + return $mq->engine->consume($channel, $queue, $callback); + }); + } + + /** + * ensure. + * + * @param mixed $channel + * @param mixed $queue + * + * @return void + */ + public static function ensure($channel, $queue) { + /** @var MessageQueue $mq */ + $mq = self::getInstance(); + + $mq->invokeEngine(function () use ($mq, $channel, $queue) { + $mq->engine->ensure($channel, $queue); + }); + } + + /** + * publish. + * + * @param mixed $channel + * @param mixed $queue + * @param mixed $message + * + * @return void + */ + public static function publish($channel, $queue, $message) { + /** @var MessageQueue $mq */ + $mq = self::getInstance(); + + $mq->invokeEngine(function () use ($mq, $channel, $queue, $message) { + $mq->engine->publish($channel, $queue, $message); + }); + } + + /** + * setEngine. + * + * @param \vhs\messaging\Engine $engine + * + * @return void + */ + public static function setEngine(Engine $engine) { + /** @var MessageQueue $mq */ + $mq = self::getInstance(); + + $mq->setEngineInternal($engine); + } + + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public static function setLogger(Logger $logger) { + /** @var MessageQueue $mq */ + $mq = self::getInstance(); + + $mq->setLoggerInternal($logger); + } + + /** + * setRethrow. + * + * @param mixed $rethrow + * + * @return void + */ + public static function setRethrow($rethrow) { + /** @var MessageQueue $mq */ + $mq = self::getInstance(); + + $mq->setRethrowInternal($rethrow); + } + + /** + * handleException. + * + * @param mixed $exception + * + * @return void + */ + private function handleException($exception) { + $this->logger->log($exception); + + if ($this->rethrow) { + throw $exception; + } + } + + /** + * invokeEngine. + * + * @param callable $func + * + * @return mixed + */ + private function invokeEngine(callable $func) { + try { + $this->engine->connect(); + } catch (\Exception $ex) { + $this->handleException($ex); + } + + $retval = null; + + try { + $retval = $func(); + } catch (\Exception $ex) { + $this->handleException($ex); + } + + return $retval; + } + + /** + * setEngineInternal. + * + * @param \vhs\messaging\Engine $engine + * + * @return void + */ + private function setEngineInternal(Engine $engine) { + if (!is_null($this->engine)) { + $this->engine->disconnect(); + } + + $this->engine = $engine; + } + + /** + * setLoggerInternal. + * + * @param \vhs\Logger $logger + * + * @return void + */ + private function setLoggerInternal(Logger $logger) { + $this->logger = $logger; + } + + /** + * setRethrowInternal. + * + * @param mixed $rethrow + * + * @return void + */ + private function setRethrowInternal($rethrow) { + $this->rethrow = $rethrow; + } +} diff --git a/packages/backend-php/vhs/messaging/engines/RabbitMQ/RabbitMQConnectionInfo.php b/packages/backend-php/vhs/messaging/engines/RabbitMQ/RabbitMQConnectionInfo.php new file mode 100644 index 00000000..64d65b15 --- /dev/null +++ b/packages/backend-php/vhs/messaging/engines/RabbitMQ/RabbitMQConnectionInfo.php @@ -0,0 +1,131 @@ +host = $host; + $this->port = $port; + $this->username = $username; + $this->password = $password; + $this->vhost = $vhost; + + // TODO throw argument exceptions here if shit is rotten + } + + /** + * getDetails. + * + * @return array{host:string,port:int,username:string,password:string,vhost:string} + */ + public function getDetails() { + return [ + 'host' => $this->host, + 'port' => $this->port, + 'username' => $this->username, + 'password' => $this->password, + 'vhost' => $this->vhost + ]; + } + + /** + * getHost. + * + * @return string + */ + public function getHost() { + return $this->host; + } + + /** + * getPassword. + * + * @return string + */ + public function getPassword() { + return $this->password; + } + + /** + * getPort. + * + * @return int + */ + public function getPort() { + return $this->port; + } + + /** + * getUsername. + * + * @return string + */ + public function getUsername() { + return $this->username; + } + + /** + * getVHost. + * + * @return string + */ + public function getVHost() { + return $this->vhost; + } +} diff --git a/packages/backend-php/vhs/messaging/engines/RabbitMQ/RabbitMQEngine.php b/packages/backend-php/vhs/messaging/engines/RabbitMQ/RabbitMQEngine.php new file mode 100644 index 00000000..0be10524 --- /dev/null +++ b/packages/backend-php/vhs/messaging/engines/RabbitMQ/RabbitMQEngine.php @@ -0,0 +1,207 @@ +info = $connectionInfo; + + $this->logger = new SilentLogger(); + } + + /** + * connect. + * + * @return void + */ + public function connect() { + if (!is_null($this->connection)) { + if ($this->connection->isConnected()) { + return; + } else { + $this->disconnect(); + } + } + + $this->channels = []; + + $this->connection = new AMQPStreamConnection( + $this->info->getHost(), + $this->info->getPort(), + $this->info->getUsername(), + $this->info->getPassword(), + $this->info->getVHost() + ); + } + + /** + * disconnect. + * + * @return void + */ + public function disconnect() { + if (!is_null($this->channels)) { + /** @var AMQPChannel $channel */ + foreach ($this->channels as $channel) { + $channel->close(); + } + } + + if (!is_null($this->connection)) { + $this->connection->close(); + } + + unset($this->channels, $this->connection); + } + + /** + * consume. + * + * @param mixed $channel + * @param mixed $queue + * @param callable $callback + * + * @return string + */ + public function consume($channel, $queue, callable $callback) { + $ch = $this->getChannel($channel); + + $ch->queue_declare($channel . '.' . $queue, false, false, false, false); + + return $ch->basic_consume($channel . '.' . $queue, \uniqid(), false, false, false, false, function ($msg) use ($callback) { + $callback($msg->body); + }); + } + + /** + * ensure. + * + * @param mixed $channel + * @param mixed $queue + * + * @return void + */ + public function ensure($channel, $queue) { + $ch = $this->getChannel($channel); + + $ch->queue_declare($channel . '.' . $queue, false, false, false, false); + } + + /** + * hasCallbacks. + * + * @param mixed $channel + * + * @return int + */ + public function hasCallbacks($channel) { + $ch = $this->getChannel($channel); + + return count($ch->callbacks); + } + + /** + * publish. + * + * @param mixed $channel + * @param mixed $queue + * @param mixed $message + * + * @return void + */ + public function publish($channel, $queue, $message) { + $ch = $this->getChannel($channel); + + $ch->queue_declare($channel . '.' . $queue, false, false, false, false); + + $ch->basic_publish(new AMQPMessage($message), '', $channel . '.' . $queue); + } + + /** + * setLogger. + * + * @param \vhs\Logger $logger + * + * @return void + */ + public function setLogger(Logger $logger) { + $this->logger = $logger; + } + + /** + * wait. + * + * @param mixed $channel + * + * @return mixed + */ + public function wait($channel) { + $ch = $this->getChannel($channel); + + return $ch->wait(); + } + + /** + * @param string $channel + * + * @return AMQPChannel + */ + private function getChannel($channel) { + /* + if (array_key_exists($channel, $this->channels)) { + return $this->channels[$channel]; + } + + $ch = $this->connection->channel(); + + $this->channels[$channel] = $ch; + + return $ch; + */ + return $this->connection->channel(); + } +} diff --git a/vhs/messaging/exceptions/MessageQueueException.php b/packages/backend-php/vhs/messaging/exceptions/MessageQueueException.php similarity index 90% rename from vhs/messaging/exceptions/MessageQueueException.php rename to packages/backend-php/vhs/messaging/exceptions/MessageQueueException.php index d2280a38..f6a79aa0 100644 --- a/vhs/messaging/exceptions/MessageQueueException.php +++ b/packages/backend-php/vhs/messaging/exceptions/MessageQueueException.php @@ -9,5 +9,6 @@ namespace vhs\messaging\exceptions; +/** @typescript */ class MessageQueueException extends \Exception { } diff --git a/packages/backend-php/vhs/migration/Backup.php b/packages/backend-php/vhs/migration/Backup.php new file mode 100644 index 00000000..aa96068e --- /dev/null +++ b/packages/backend-php/vhs/migration/Backup.php @@ -0,0 +1,239 @@ +server = $server; + $this->user = $user; + $this->password = $password; + $this->database = $database; + + if (is_null($logger)) { + $this->logger = new SilentLogger(); + } else { + $this->logger = $logger; + } + } + + /** + * external_backup. + * + * @param bool $do_host + * @param string $fileName + * @param string $backupPath + * + * @return bool + */ + public function external_backup($do_host = false, $fileName = null, $backupPath = 'backup/') { + $this->logger->log('Starting backup'); + + $fileName = !is_null($fileName) ? $fileName : sprintf('db-backup-%s.sql', time()); + + $command = []; + $command[] = 'mysqldump'; + $command[] = sprintf("-u '%s'", $this->user); + $command[] = sprintf("-p '%s'", $this->password); + if ($do_host == true) { + $command[] = '--host'; + $command[] = $this->server; + $command[] = '--skip-ssl-verify-server-cert'; + } + $command[] = sprintf("'%s'", $this->database); + $command[] = '>'; + $command[] = sprintf("'%s%s'", $backupPath, $fileName); + + exec(implode(' ', $command), $output, $return); + + return !$return; + } + + /** + * internal_backup. + * + * @param string $fileName + * @param string $backupPath + * + * @return bool + */ + public function internal_backup($fileName = null, $backupPath = 'backup/') { + $this->logger->log('Starting backup'); + + $conn = new \mysqli($this->server, $this->user, $this->password); + + if ($conn->connect_error) { + $this->logger->log('Connection failed: ' . $conn->connect_error); + + return false; + } + + $conn->select_db($this->database); + + $return = ''; + $tables = []; + $sql = 'SHOW TABLES'; + $i = 0; + $result = $conn->query($sql); + if (!$result) { + return false; + } + + //Grab all tables + while ($row = $result->fetch_array()) { + $tables[] = $row[0]; + $i++; + } + + $this->logger->log('Database tables: ' . $i); + + foreach ($tables as $table) { + $sql = 'SHOW CREATE TABLE `' . $table . '`;'; + print 'Grabbing table ' . $table . ': '; + + $create_result = $conn->query($sql); + + if (!$create_result) { + return false; + } + + $create_row = $create_result->fetch_row(); + $return .= $create_row[1]; + + $sql = 'SELECT * FROM `' . $table . '`;'; + + $result = $conn->query($sql); + if (!$result) { + return false; + } + + $num_fields = @intval($result->num_rows); + + $return .= ";\nINSERT INTO `" . $table . '` VALUES('; + + $types = $result->fetch_fields(); + + //Grab all keys + for ($i = 0; ($keys = $result->fetch_row()); $i++) { + if ($i) { + $return .= "),\n("; + } + + $firstKey = 1; + $e = 0; + foreach ($keys as $key) { + $key = $conn->escape_string($key); + + if ($this->is_mysql_num($types[$e]->type)) { + if (!$key && $key !== 0 && $key !== '0') { + $key = 'NULL'; + } + } else { + $key = '\'' . $key . '\''; + } + + if (!$firstKey) { + $return .= ' , '; + } + $return .= $key; + $firstKey = 0; + $e++; + } + } + $return .= ");\n"; + + print $i . " keys.\n"; + } + + $fileName = !is_null($fileName) ? $fileName : 'db-backup-' . time() . '-' . md5($return) . '.sql'; + $handle = fopen($backupPath . $fileName, 'w+'); + fwrite($handle, $return); + fclose($handle); + + return true; + } + + /** + * is_mysql_num. + * + * @param mixed $type + * + * @return bool + */ + private function is_mysql_num($type) { + //http://php.net/manual/en/mysqli-result.fetch-field.php + + switch ($type) { + case 1: //TINYINT / BOOL + case 2: //SMALLINT + case 3: //INTEGER + case 4: //FLOAT + case 5: //DOUBLE + //case 7: //TIMESTAMP - not an int, but treated similar here + case 8: //BIGINT / SERIAL + case 9: //MEDIUMINT + case 16: //BIT + case 246: //DECIMAL / NUMERIC / FIXED + return true; + + default: + return false; + } + } +} diff --git a/vhs/migration/Migrator.php b/packages/backend-php/vhs/migration/Migrator.php similarity index 75% rename from vhs/migration/Migrator.php rename to packages/backend-php/vhs/migration/Migrator.php index a2e6efc5..6795e1d8 100644 --- a/vhs/migration/Migrator.php +++ b/packages/backend-php/vhs/migration/Migrator.php @@ -12,14 +12,61 @@ use vhs\Logger; use vhs\loggers\SilentLogger; +/** @typescript */ class Migrator { + /** + * command options. + * + * @var string[] + */ + private array $cmd_opts = []; + /** + * database. + * + * @var string + */ private $database; + + /** + * logger. + * + * @var \vhs\Logger + */ private $logger; + + /** + * password. + * + * @var string + */ private $password; + + /** + * server. + * + * @var string + */ private $server; + + /** + * user. + * + * @var string + */ private $user; - public function __construct($server, $user, $password, $database, Logger $logger = null) { + /** + * __construct. + * + * @param string $server + * @param string $user + * @param string $password + * @param string $database + * @param \vhs\Logger $logger + * + * @return void + */ + public function __construct($server, $user, $password, $database, ?Logger $logger = null) { $this->server = $server; $this->user = $user; $this->password = $password; @@ -30,8 +77,20 @@ public function __construct($server, $user, $password, $database, Logger $logger } else { $this->logger = $logger; } + + if (getenv('MIGRATOR_SKIP_SSL', true) !== false) { + array_push($this->cmd_opts, '--skip-ssl'); + } } + /** + * migrate. + * + * @param int|null $toVersion + * @param string $migrationsPath + * + * @return bool + */ public function migrate($toVersion = null, $migrationsPath = '.') { $this->logger->log('Starting migration'); @@ -39,6 +98,7 @@ public function migrate($toVersion = null, $migrationsPath = '.') { if ($conn->connect_error) { $this->logger->log('Connection failed: ' . $conn->connect_error); + return false; } @@ -47,6 +107,7 @@ public function migrate($toVersion = null, $migrationsPath = '.') { $this->logger->log('Database created successfully'); } else { $this->logger->log('Error creating database: ' . $conn->error); + return false; } @@ -73,17 +134,20 @@ public function migrate($toVersion = null, $migrationsPath = '.') { if ($toVersion > $versions[sizeof($versions) - 1]) { $this->logger->log('Cannot target a version higher than latest.'); + return false; } if ($toVersion <= $currentversion) { $this->logger->log('Target must be higher than current.'); + return false; } } if ($currentversion == $versions[sizeof($versions) - 1]) { $this->logger->log('Already up to date.'); + return true; } @@ -98,14 +162,22 @@ public function migrate($toVersion = null, $migrationsPath = '.') { continue; } - //TODO these should prob be in a transaction to allow rollback in case a migration fails. + // TODO these should prob be in a transaction to allow rollback in case a migration fails. $this->logger->log('Upgrading to: ' . $version); $scripts = array_values(array_diff(scandir($migrationsPath . '/' . $version, SCANDIR_SORT_ASCENDING), ['..', '.'])); $script_path = $migrationsPath . '/' . $version . '/'; - $command = 'mysql -u' . DB_USER . ' -p' . DB_PASS . ' ' . '-h ' . DB_SERVER . ' -D ' . DB_DATABASE . " < {$script_path}"; + $command = sprintf( + "mysql -u %s -p %s -h %s -D %s %s < %s\n", + DB_USER, + DB_PASS, + DB_SERVER, + DB_DATABASE, + implode(' ', $this->cmd_opts), + $script_path + ); foreach ($scripts as $script) { $this->logger->log('Executing: ' . $script); @@ -114,6 +186,7 @@ public function migrate($toVersion = null, $migrationsPath = '.') { $output = shell_exec($command . $script); } catch (\Exception $e) { $this->logger->log('Caught exception: ' . $e->getMessage() . "\n"); + return false; } diff --git a/packages/backend-php/vhs/monitors/Monitor.php b/packages/backend-php/vhs/monitors/Monitor.php new file mode 100644 index 00000000..834a0b0d --- /dev/null +++ b/packages/backend-php/vhs/monitors/Monitor.php @@ -0,0 +1,29 @@ +token = $token; + } + + /** + * getToken. + * + * @return string + */ + public function getToken() { + return $this->token; + } +} diff --git a/packages/backend-php/vhs/security/CurrentUser.php b/packages/backend-php/vhs/security/CurrentUser.php new file mode 100644 index 00000000..8bf420fc --- /dev/null +++ b/packages/backend-php/vhs/security/CurrentUser.php @@ -0,0 +1,136 @@ +currentPrincipal = new AnonPrincipal(); + } + + /** + * canGrantAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + final public static function canGrantAllPermissions(...$permission) { + return CurrentUser::getPrincipal()->canGrantAllPermissions(...$permission); + } + + /** + * canGrantAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + final public static function canGrantAnyPermissions(...$permission) { + return CurrentUser::getPrincipal()->canGrantAnyPermissions(...$permission); + } + + /** + * getIdentity. + * + * @return mixed + */ + final public static function getIdentity() { + return CurrentUser::getPrincipal()->getIdentity(); + } + + /** + * getPrincipal. + * + * @return \vhs\security\IPrincipal + */ + final public static function getPrincipal() { + return CurrentUser::getInstance()->currentPrincipal; + } + + /** + * hasAllPermissions. + * + * @param string ...$permission + * + * @return bool + */ + final public static function hasAllPermissions(...$permission) { + return CurrentUser::getPrincipal()->hasAllPermissions(...$permission); + } + + /** + * hasAnyPermissions. + * + * @param string ...$permission + * + * @return bool + */ + final public static function hasAnyPermissions(...$permission) { + return CurrentUser::getPrincipal()->hasAnyPermissions(...$permission); + } + + /** + * isAnon. + * + * @return bool + */ + final public static function isAnon() { + return CurrentUser::getPrincipal()->isAnon(); + } + + /** + * setPrincipal. + * + * @param \vhs\security\IPrincipal $principal + * + * @return void + */ + final public static function setPrincipal($principal) { + CurrentUser::getInstance()->currentPrincipal = $principal; + } + + /** + * getInstance. + * + * @return \vhs\security\CurrentUser + */ + final protected static function getInstance() { + static $aoInstance = []; + + if (session_status() === PHP_SESSION_ACTIVE) { + if (!isset($_SESSION['CurrentUser'])) { + $_SESSION['CurrentUser'] = new CurrentUser(); + } + + return $_SESSION['CurrentUser']; + } else { + if (!isset($aoInstance['CurrentUser'])) { + $aoInstance['CurrentUser'] = new CurrentUser(); + } + + return $aoInstance['CurrentUser']; + } + } + + /** + * __clone. + * + * @return void + */ + public function __clone(): void { + } +} diff --git a/packages/backend-php/vhs/security/IAuthenticate.php b/packages/backend-php/vhs/security/IAuthenticate.php new file mode 100644 index 00000000..fba83ef8 --- /dev/null +++ b/packages/backend-php/vhs/security/IAuthenticate.php @@ -0,0 +1,39 @@ +username = $username; + $this->password = $password; + } + + /** + * getPassword. + * + * @return string + */ + public function getPassword() { + return $this->password; + } + + /** + * getUsername. + * + * @return string + */ + public function getUsername() { + return $this->username; + } +} diff --git a/packages/backend-php/vhs/security/exceptions/InvalidCredentials.php b/packages/backend-php/vhs/security/exceptions/InvalidCredentials.php new file mode 100644 index 00000000..f6a89ac2 --- /dev/null +++ b/packages/backend-php/vhs/security/exceptions/InvalidCredentials.php @@ -0,0 +1,20 @@ +context = $context; + } + + /** + * Shared wrapper to throw a DomainException when a service handler domain class is not found. + * + * @throws \vhs\exceptions\HttpException + * + * @return void + */ + protected function throwNotFound() { + $className = get_called_class(); + + throw new HttpException(sprintf('%s not found', $className), HttpStatusCodes::Client_Error_Not_Found); + } +} diff --git a/packages/backend-php/vhs/services/ServiceClient.php b/packages/backend-php/vhs/services/ServiceClient.php new file mode 100644 index 00000000..b5ca1cb1 --- /dev/null +++ b/packages/backend-php/vhs/services/ServiceClient.php @@ -0,0 +1,37 @@ +discover($uri, true); + + $data = array_combine($contract->methods->$method, $arguments); + + return ServiceRegistry::get($namespace)->handle($uri . '/' . $method, $data, true); + } +} diff --git a/packages/backend-php/vhs/services/ServiceContext.php b/packages/backend-php/vhs/services/ServiceContext.php new file mode 100644 index 00000000..1d72077d --- /dev/null +++ b/packages/backend-php/vhs/services/ServiceContext.php @@ -0,0 +1,50 @@ +endpoint = $endpoint; + } + + /** + * log. + * + * @param mixed $message + * + * @return void + */ + public function log($message) { + if (is_null($this->endpoint->logger)) { + return; + } + + $type = get_class($this->endpoint); + + $this->endpoint->logger->log("[{$type}] $message"); + } +} diff --git a/packages/backend-php/vhs/services/ServiceHandler.php b/packages/backend-php/vhs/services/ServiceHandler.php new file mode 100644 index 00000000..6547f154 --- /dev/null +++ b/packages/backend-php/vhs/services/ServiceHandler.php @@ -0,0 +1,188 @@ +logger = &$logger; + $this->endpointNamespace = $endpointNamespace; + $this->rootNamespacePath = is_null($rootNamespacePath) ? dirname(__FILE__) : $rootNamespacePath; + $this->uriPrefixPath = $uriPrefixPath; + + SplClassLoader::getInstance()->add(new SplClassLoaderItem($this->endpointNamespace, $this->rootNamespacePath, '.svc.php')); + } + + /** + * discover. + * + * @param string $uri + * @param bool $isNative + * + * @return mixed + */ + public function discover($uri, $isNative = false) { + /** @var Endpoint $endpoint */ + $endpoint = $this->getEndpoint($uri); + + $out = $endpoint::getInstance()->discover(); + + if ($isNative) { + return $endpoint::getInstance()->deserializeOutput($out); + } else { + return $out; + } + } + + /** + * Get all endpoints. + * + * @return string[] + */ + public function getAllEndpoints() { + $files = scandir($this->rootNamespacePath . '/' . str_replace('\\', '/', $this->endpointNamespace)); + + $endpoints = []; + + foreach ($files as $file) { + if (preg_match('%(?P.*)\.svc.php%im', $file, $matches)) { + /** @var class-string $endpoint */ + $endpoint = $this->endpointNamespace . '\\' . $matches['endpoint']; + array_push($endpoints, $endpoint::getInstance()); + } + } + + return $endpoints; + } + + /** + * handle. + * + * @param string $uri + * @param mixed $data + * @param bool $isNative + * + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return mixed + */ + public function handle($uri, $data = null, $isNative = false) { + /** @var Endpoint[] $endpoints */ + $endpoints = []; + + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('handling: %s with prefixpath %s', $uri, $this->uriPrefixPath)); + + if (!preg_match('%.*/' . $this->uriPrefixPath . '(?P.*)\.svc/(?P.*)%im', $uri, $regs)) { + if (!preg_match('%.*/' . $this->uriPrefixPath . '(?P.*)\.svc%im', $uri, $regs)) { + if (preg_match('%.*/' . $this->uriPrefixPath . 'help%im', $uri, $regs)) { + $files = scandir($this->rootNamespacePath . '/' . str_replace('\\', '/', $this->endpointNamespace)); + + foreach ($files as $file) { + if (preg_match('%(?P.*)\.svc.php%im', $file, $matches)) { + array_push($endpoints, $matches['endpoint']); + } + } + } else { + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('did not find match for: %s', $uri)); + + throw new InvalidRequestException('Invalid service request', HttpStatusCodes::Client_Error_Misdirected_Request); + } + } + } + + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('$endpoints => %s', json_encode($endpoints))); + + if (count($endpoints) > 0) { + $discovery = []; + + foreach ($endpoints as $class) { + /** @var class-string $endpoint */ + $endpoint = $this->endpointNamespace . '\\' . $class; + + $discovery[$class . '.svc'] = $endpoint::getInstance()->deserializeOutput($endpoint::getInstance()->discover()); + } + + /*TODO this is a hack. Each endpoint in a namespace could have a totally different + * type of serializer and here we're effectively assuming that returning json encoded + * data is ok. This is probably fine in all cases but not ideal. The caller doesn't know that + * help was requested and that the return type is going to be the result of a service discovery + */ + return json_encode($discovery); + } else { + /** @var class-string $endpoint */ + $endpoint = $this->endpointNamespace . '\\' . $regs['endpoint']; + + if (array_key_exists('method', $regs)) { + $args = $data; + if ($isNative) { + $args = $endpoint::getInstance()->serializeInput($data); + } + + $endpoint::getInstance()->logger = &$this->logger; + + $method = $regs['method']; + $out = $endpoint::getInstance()->handleRequest($method, $args); + } else { + $out = $endpoint::getInstance()->discover(); + } + + if ($isNative) { + return $endpoint::getInstance()->deserializeOutput($out); + } else { + return $out; + } + } + } + + /** + * getEndpoint. + * + * @param string $uri + * + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return string + */ + private function getEndpoint($uri) { + if (!preg_match('%.*/' . $this->uriPrefixPath . '(?P.*)\.svc%im', $uri, $regs)) { + throw new InvalidRequestException('Invalid endpoint request', HttpStatusCodes::Client_Error_Im_a_teapot); + } + + return $this->endpointNamespace . '\\' . $regs['endpoint']; + } +} diff --git a/packages/backend-php/vhs/services/ServiceRegistry.php b/packages/backend-php/vhs/services/ServiceRegistry.php new file mode 100644 index 00000000..e6e6403a --- /dev/null +++ b/packages/backend-php/vhs/services/ServiceRegistry.php @@ -0,0 +1,44 @@ + */ + private $services = []; + + /** + * @param string $key + * + * @return ServiceHandler + */ + final public static function get($key) { + return ServiceRegistry::getInstance()->services[$key]; + } + + /** + * @param \vhs\Logger $logger + * @param string $key + * @param string $namespace + * @param string $path + * + * @return void + */ + final public static function register(Logger &$logger, $key, $namespace, $path) { + ServiceRegistry::getInstance()->services[$key] = new ServiceHandler($logger, $namespace, $path, $key . '/'); + } +} diff --git a/vhs/services/endpoints/Endpoint.php b/packages/backend-php/vhs/services/endpoints/Endpoint.php similarity index 81% rename from vhs/services/endpoints/Endpoint.php rename to packages/backend-php/vhs/services/endpoints/Endpoint.php index c7121956..e2a5495a 100644 --- a/vhs/services/endpoints/Endpoint.php +++ b/packages/backend-php/vhs/services/endpoints/Endpoint.php @@ -9,25 +9,39 @@ namespace vhs\services\endpoints; -use vhs\Logger; use vhs\security\CurrentUser; use vhs\security\exceptions\UnauthorizedException; use vhs\services\exceptions\InvalidContractException; use vhs\services\exceptions\InvalidRequestException; use vhs\services\IContract; +/** @typescript */ abstract class Endpoint implements IEndpoint { - /** @var Logger $logger */ + /** @var \vhs\Logger $logger */ public $logger; + /** + * internal_service. + * + * @var mixed + */ protected $internal_service; + /** + * __construct. + * + * @param \vhs\services\IContract $service + * + * @return void + */ protected function __construct(IContract $service) { $this->internal_service = $service; } /** - * @return Endpoint + * getInstance. + * + * @return \vhs\services\endpoints\Endpoint */ final public static function getInstance() { static $aoInstance = []; @@ -41,6 +55,11 @@ final public static function getInstance() { return $aoInstance[$class]; } + /** + * discover. + * + * @return mixed + */ final public function discover() { $contract = $this->getContract(); @@ -59,6 +78,11 @@ final public function discover() { return $this->serializeOutput((object) $out); } + /** + * getAllPermissions. + * + * @return array + */ final public function getAllPermissions() { $contract = $this->getContract(); @@ -71,6 +95,18 @@ final public function getAllPermissions() { return $allPermissions; } + /** + * handleRequest. + * + * @param mixed $method + * @param mixed $data + * + * @throws \vhs\security\exceptions\UnauthorizedException + * @throws \vhs\services\exceptions\InvalidContractException + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return mixed + */ final public function handleRequest($method, $data) { $args = $this->deserializeInput($data); @@ -141,15 +177,19 @@ final public function handleRequest($method, $data) { } /** - * @return \ReflectionClass + * getContract. * - * @throws \Exception + + * @throws \vhs\services\exceptions\InvalidContractException + * + * @return mixed */ private function getContract() { - //TODO this would be a good place to implement a memcache registry of permissions & service endpoints + // TODO this would be a good place to implement a memcache registry of permissions & service endpoints $serviceClass = new \ReflectionClass($this->internal_service); $contract = null; + foreach ($serviceClass->getInterfaces() as $interface) { if (array_key_exists('vhs\\services\\IContract', $interface->getInterfaces())) { $contract = $interface; @@ -163,6 +203,13 @@ private function getContract() { return $contract; } + /** + * getMethodPermissions. + * + * @param \ReflectionMethod $method + * + * @return string[][] + */ private function getMethodPermissions(\ReflectionMethod $method) { $comments = $method->getDocComment(); @@ -179,6 +226,11 @@ private function getMethodPermissions(\ReflectionMethod $method) { return $permissions; } - private function __clone() { + /** + * __clone. + * + * @return void + */ + public function __clone(): void { } } diff --git a/packages/backend-php/vhs/services/endpoints/IEndpoint.php b/packages/backend-php/vhs/services/endpoints/IEndpoint.php new file mode 100644 index 00000000..f866f3f4 --- /dev/null +++ b/packages/backend-php/vhs/services/endpoints/IEndpoint.php @@ -0,0 +1,49 @@ + */ + public $headers; + + /** @var string */ + public $method; + + /** @var string */ + public $url; + + /** + * __construct. + * + * @return void + */ + public function __construct() { + } +} diff --git a/packages/backend-php/vhs/web/HttpRequestHandler.php b/packages/backend-php/vhs/web/HttpRequestHandler.php new file mode 100644 index 00000000..1ccd3154 --- /dev/null +++ b/packages/backend-php/vhs/web/HttpRequestHandler.php @@ -0,0 +1,22 @@ +logger = new SilentLogger(); + } else { + $this->logger = &$logger; + } + + if (is_null($infoModule)) { + $this->register(new HttpServerInfoModule()); + } else { + $this->register($infoModule); + } + } + + /** + * clear. + * + * @return void + */ + public function clear() { + if ($this->endset) { + return; + } + + unset($this->headerBuffer); + $this->headerBuffer = []; + unset($this->outputBuffer); + $this->outputBuffer = []; + $this->http_response_code = HttpStatusCodes::Success_Ok->value; + } + + /** + * code. + * + * @param mixed $code + * + * @return void + */ + public function code($code) { + if ($this->endset) { + return; + } + + $this->http_response_code = $code; + } + + /** + * end. + * + * @return void + */ + public function end() { + $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'trying end'); + + if ($this->endset) { + $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'already ended - bailing'); + + return; + } + + $this->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); + + $this->endset = true; + $self = $this; + array_push($this->outputBuffer, function () use ($self) { + $self->endResponse(); + }); + } + + /** + * handle. + * + * @return void + */ + public function handle() { + $this->handling = true; + $this->clear(); + + $this->request = HttpUtil::getCurrentRequest(); + + session_set_cookie_params(['SameSite' => 'Lax', 'HttpOnly' => 'true']); + session_start(); + + $exception = null; + $index = 0; + + /** @var IHttpModule $module */ + foreach ($this->modules as $module) { + $this->logger->debug(__FILE__, __LINE__, __METHOD__, sprintf('trying module: %s', get_class($module))); + + if ($this->endset) { + break; + } + + try { + $module->handle($this); + } catch (\Exception $ex) { + $exception = $ex; + + break; + } + $index += 1; + } + + if (!$this->endset && is_null($exception)) { + $exception = new InvalidRequestException('No valid service endpoint found', HttpStatusCodes::Server_Error_Service_Unavailable); + } + + $this->log($this->request->method . ' ' . $this->request->url . ' ' . json_encode($this->request->headers)); + + if (!is_null($exception)) { + /** @var IHttpModule $module */ + foreach (array_reverse(array_slice($this->modules, 0, $index + 1)) as $module) { + $module->handleException($this, $exception); + } + } + + //$this->end(); + + http_response_code($this->http_response_code); + + foreach ($this->headerBuffer as $header) { + $header(); + } + + foreach ($this->outputBuffer as $output) { + $output(); + } + + $this->endResponse(); + } + + /** + * header. + * + * @param mixed $string + * @param mixed $replace + * @param mixed $http_response_code + * + * @return void + */ + public function header($string, $replace = true, $http_response_code = 0) { + if ($this->endset) { + return; + } + + $self = $this; + + array_push($this->headerBuffer, function () use ($string, $replace, $http_response_code) { + if (headers_sent() === false) { + header($string, $replace, $http_response_code); + } + }); + } + + /** + * log. + * + * @param mixed $message + * + * @return void + */ + public function log($message) { + $this->logger->log($message); + } + + /** + * output. + * + * @param mixed $data + * + * @return void + */ + public function output($data) { + if ($this->endset) { + return; + } + + array_push($this->outputBuffer, function () use ($data) { + echo $data; + }); + } + + /** + * redirect. + * + * @param string $url + * @param bool $permanent + * + * @return void + */ + public function redirect($url, $permanent = false) { + if ($this->endset) { + return; + } + + $this->header('Location: ' . $url, true, $permanent ? 301 : 302); + } + + /** + * register. + * + * @param \vhs\web\IHttpModule $module + * + * @return void + */ + public function register(IHttpModule $module) { + if ($this->handling) { + $this->log('Failed to register module ' . get_class($module)); + + throw new \Exception('Registrations must occur prior to handling a request'); + } + + array_push($this->modules, $module); + } + + /** + * sendOnlyHeaders. + * + * @return void + */ + public function sendOnlyHeaders() { + if ($this->endset) { + return; + } + + $self = $this; + array_push($this->headerBuffer, function (): never { + exit(); + }); + } + + /** + * endResponse. + * + * @return void + */ + private function endResponse() { + $exception = null; + $index = 0; + + /** @var IHttpModule $module */ + foreach ($this->modules as $module) { + try { + $module->endResponse($this); + } catch (\Exception $ex) { + $exception = $ex; + + break; + } + $index += 1; + } + + if (!is_null($exception)) { + /** @var IHttpModule $module */ + foreach (array_reverse(array_slice($this->modules, 0, $index + 1)) as $module) { + $module->handleException($this, $exception); + } + } + + exit(); + } +} diff --git a/vhs/web/HttpUtil.php b/packages/backend-php/vhs/web/HttpUtil.php similarity index 77% rename from vhs/web/HttpUtil.php rename to packages/backend-php/vhs/web/HttpUtil.php index 3f781115..da4e8544 100644 --- a/vhs/web/HttpUtil.php +++ b/packages/backend-php/vhs/web/HttpUtil.php @@ -9,9 +9,10 @@ namespace vhs\web; +/** @typescript */ class HttpUtil { /** - * @returns HttpRequest + * @return \vhs\web\HttpRequest */ public static function getCurrentRequest() { $req = new HttpRequest(); @@ -23,16 +24,32 @@ public static function getCurrentRequest() { return $req; } + /** + * getRequestMethod. + * + * @return string + */ public static function getRequestMethod() { return $_SERVER['REQUEST_METHOD']; } + /** + * getRequestUrl. + * + * @return string + */ public static function getRequestUrl() { return $_SERVER['SCRIPT_NAME']; } + /** + * parseRequestHeaders. + * + * @return array + */ public static function parseRequestHeaders() { $headers = []; + foreach ($_SERVER as $key => $value) { if (substr($key, 0, 5) != 'HTTP_') { continue; @@ -40,6 +57,7 @@ public static function parseRequestHeaders() { $header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5))))); $headers[$header] = $value; } + return $headers; } } diff --git a/packages/backend-php/vhs/web/IHttpModule.php b/packages/backend-php/vhs/web/IHttpModule.php new file mode 100644 index 00000000..0bc13af2 --- /dev/null +++ b/packages/backend-php/vhs/web/IHttpModule.php @@ -0,0 +1,41 @@ +realm = $realm; + $this->authorizer = $authorizer; + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function handle(HttpServer $server) { + if (array_key_exists('PHP_AUTH_USER', $_SERVER) && $_SERVER['PHP_AUTH_USER'] && !$this->authorizer->isAuthenticated()) { + try { + $this->authorizer->login(new UserPassCredentials($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])); + } catch (\Exception $ex) { + //$this->requestAuth($server, $ex->getMessage()); + + $server->log('Login attempt failed: ' . $ex->getMessage()); + $server->clear(); + $server->code(500); + $server->output($ex->getMessage()); + $server->end(); + } + } + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + if (get_class($ex) === 'vhs\\security\\exceptions\\UnauthorizedException') { + $this->requestAuth($server, $ex->getMessage()); + } + } + + /** + * requestAuth. + * + * @param \vhs\web\HttpServer $server + * @param mixed $message + * + * @return void + */ + private function requestAuth(HttpServer $server, $message) { + $server->clear(); + $server->header('WWW-Authenticate: Basic realm="' . $this->realm . '"'); + $server->header('HTTP/1.0 401 Unauthorized'); + $server->code(401); + $server->output($message); + $server->end(); + } +} diff --git a/packages/backend-php/vhs/web/modules/HttpBearerTokenAuthModule.php b/packages/backend-php/vhs/web/modules/HttpBearerTokenAuthModule.php new file mode 100644 index 00000000..c22169c9 --- /dev/null +++ b/packages/backend-php/vhs/web/modules/HttpBearerTokenAuthModule.php @@ -0,0 +1,93 @@ +authorizer = $authorizer; + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function handle(HttpServer $server) { + $bearerToken = null; + + $authorization = array_key_exists($this->headerKey, $_SERVER) ? $_SERVER[$this->headerKey] : null; + + if (!is_null($authorization) && substr($authorization, 0, 7) === 'Bearer ') { + $bearerToken = substr($authorization, 7, strlen($authorization)); + } + + if (!is_null($bearerToken)) { + try { + $this->authorizer->login(new BearerTokenCredentials($bearerToken)); + } catch (\Exception $ex) { + $server->log('Login attempt failed: ' . $ex->getMessage()); + $server->clear(); + $server->code(500); + $server->output($ex->getMessage()); + $server->end(); + } + } + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + } +} diff --git a/packages/backend-php/vhs/web/modules/HttpExceptionHandlerModule.php b/packages/backend-php/vhs/web/modules/HttpExceptionHandlerModule.php new file mode 100644 index 00000000..1b11d8de --- /dev/null +++ b/packages/backend-php/vhs/web/modules/HttpExceptionHandlerModule.php @@ -0,0 +1,90 @@ +level = $level; + $this->logger = $logger; + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function handle(HttpServer $server) { + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + $this->logger->log($ex->getMessage()); + $this->logger->log($ex->getTraceAsString()); + + $server->code($ex->getCode() !== 0 ? $ex->getCode() : 500); + + // @phpstan-ignore if.alwaysFalse + if (DEBUG) { + $server->output($ex->getMessage()); + $server->output($ex->getTraceAsString()); + } else { + $server->output($ex->getMessage()); + } + } +} diff --git a/packages/backend-php/vhs/web/modules/HttpJsonServiceHandlerModule.php b/packages/backend-php/vhs/web/modules/HttpJsonServiceHandlerModule.php new file mode 100644 index 00000000..99b54a02 --- /dev/null +++ b/packages/backend-php/vhs/web/modules/HttpJsonServiceHandlerModule.php @@ -0,0 +1,122 @@ +registryKey = $registryKey; + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @throws \vhs\services\exceptions\InvalidRequestException + * + * @return void + */ + public function handle(HttpServer $server) { + $input = null; + + $server->header('Content-Type: application/json', true); + + $uri = $server->request->url; + + try { + switch ($server->request->method) { + case 'HEAD': + $server->output(ServiceRegistry::get($this->registryKey)->discover($uri)); + + $server->end(); + + break; + case 'GET': + if (isset($_GET['json'])) { + $input = $_GET['json']; + } else { + $input = json_encode($_GET); + } + + $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, $input)); + + break; + case 'POST': + $server->output(ServiceRegistry::get($this->registryKey)->handle($uri, file_get_contents('php://input'))); + + $server->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); + + break; + //case 'PUT': + //case 'DELETE': + default: + throw new InvalidRequestException(); + // TODO clean up + // break; + } + + $server->logger->debug(__FILE__, __LINE__, __METHOD__, 'setting end'); + $server->end(); + } catch (\Throwable $exception) { + $server->logger->debug( + __FILE__, + __LINE__, + __METHOD__, + sprintf('caught exception: %s(%s)', $exception->getMessage(), $exception->getCode()) + ); + + if ($exception->getCode() !== HttpStatusCodes::Client_Error_Misdirected_Request->value) { + throw $exception; + } + } + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + } +} diff --git a/packages/backend-php/vhs/web/modules/HttpRequestHandlerModule.php b/packages/backend-php/vhs/web/modules/HttpRequestHandlerModule.php new file mode 100644 index 00000000..9b5434da --- /dev/null +++ b/packages/backend-php/vhs/web/modules/HttpRequestHandlerModule.php @@ -0,0 +1,85 @@ +> + */ + private $registry = []; + + /** + * __construct. + * + * @return void + */ + public function __construct() { + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function handle(HttpServer $server) { + if (array_key_exists($server->request->method, $this->registry)) { + if (array_key_exists($server->request->url, $this->registry[$server->request->method])) { + $this->registry[$server->request->method][$server->request->url]->handle($server); + } + } + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + } + + /** + * register_internal. + * + * @param string $method + * @param string $url + * @param \vhs\web\HttpRequestHandler $handler + * + * @return void + */ + protected function register_internal($method, $url, HttpRequestHandler $handler) { + if (!array_key_exists($method, $this->registry)) { + $this->registry[$method] = []; + } + + $this->registry[$method][$url] = $handler; + } +} diff --git a/packages/backend-php/vhs/web/modules/HttpServerInfoModule.php b/packages/backend-php/vhs/web/modules/HttpServerInfoModule.php new file mode 100644 index 00000000..666f8db4 --- /dev/null +++ b/packages/backend-php/vhs/web/modules/HttpServerInfoModule.php @@ -0,0 +1,66 @@ +name = $name; + } + + /** + * endResponse. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function endResponse(HttpServer $server) { + } + + /** + * handle. + * + * @param \vhs\web\HttpServer $server + * + * @return void + */ + public function handle(HttpServer $server) { + $server->header('Server: ' . $this->name); + } + + /** + * handleException. + * + * @param \vhs\web\HttpServer $server + * @param \Exception $ex + * + * @return void + */ + public function handleException(HttpServer $server, \Exception $ex) { + } +} diff --git a/packages/frontend-react/.bowerrc b/packages/frontend-react/.bowerrc new file mode 100644 index 00000000..5abcb495 --- /dev/null +++ b/packages/frontend-react/.bowerrc @@ -0,0 +1,5 @@ +{ + "directory": "./public/assets/", + "save": true, + "save-exact": true +} diff --git a/packages/frontend-react/.editorconfig b/packages/frontend-react/.editorconfig new file mode 100644 index 00000000..8ae4323a --- /dev/null +++ b/packages/frontend-react/.editorconfig @@ -0,0 +1,9 @@ +[*] +indent_style=space +indent_size=4 +end_of_line=lf +charset=utf-8 +trim_trailing_whitespace=true +insert_final_newline=true +max_line_width=120 +print_width=120 diff --git a/packages/frontend-react/.gitignore b/packages/frontend-react/.gitignore new file mode 100644 index 00000000..58e00791 --- /dev/null +++ b/packages/frontend-react/.gitignore @@ -0,0 +1,58 @@ +* +!.gitignore + +!.storybook/ +!.storybook/* +!.storybook/ + +!conf/ +!conf/* + +!docs/ +!docs/*.md + +!public/ +!public/**/ +!public/**/* +public/assets/ +public/config.json + +!skel/ +!skel/**/ +!skel/**/* + +!src/ +!src/**/ +!src/**/*.css +!src/**/*.ts +!src/**/*.tsx +src/routeTree.gen.ts +src/lib/ui/fontawesome/generated.ts +src/routes/**/fafo*.tsx +src/routes/**/test*.tsx +src/types/fontawesome/generated.ts +src/types/nomos.d.ts + +!tools/ +!tools/* + +!.bowerrc +!.editorconfig +!.prettierignore +!bower.json +!eslint.config.mjs +!generate-react-cli.json +!postcss.config.js +!prettier.config.mjs +!stylelint.config.mjs +!tailwind.config.js +!tsconfig.*.json +!tsconfig.json +!vite.config.ts + +!index.html + +!package.json +!pnpm-lock.yaml + +!README.md diff --git a/packages/frontend-react/.prettierignore b/packages/frontend-react/.prettierignore new file mode 100644 index 00000000..d23df342 --- /dev/null +++ b/packages/frontend-react/.prettierignore @@ -0,0 +1,5 @@ +pnpm-lock.yaml +src/routeTree.gen.ts +*.svg +public/assets/ +src/types/nomos.d.ts diff --git a/packages/frontend-react/.storybook/main.ts b/packages/frontend-react/.storybook/main.ts new file mode 100644 index 00000000..d6a5ef11 --- /dev/null +++ b/packages/frontend-react/.storybook/main.ts @@ -0,0 +1,52 @@ +import { mergeConfig } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +import type { StorybookConfig } from '@storybook/react-vite' + +const includeDirs = [ + '01-atoms', + '02-molecules', + '03-particles', + '04-composites', + '05-materials', + '06-layouts', + '07-pages', + '08-app', + '09-providers' +] + +const stories = [ + '../src/**/*.mdx', + ...includeDirs.map((includeDir) => `../src/components/${includeDir}/**/*.stories.@(js|jsx|mjs|ts|tsx)`) +] + +const config: StorybookConfig = { + stories, + + addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], + + framework: { + name: '@storybook/react-vite', + options: {} + }, + + docs: {}, + + core: { + disableTelemetry: true + }, + + staticDirs: ['../public'], + + typescript: { + reactDocgen: 'react-docgen-typescript' + }, + + viteFinal(config) { + return mergeConfig(config, { + plugins: [tsconfigPaths()] + }) + } +} + +export default config diff --git a/packages/frontend-react/.storybook/preview-head.html b/packages/frontend-react/.storybook/preview-head.html new file mode 100644 index 00000000..2882e410 --- /dev/null +++ b/packages/frontend-react/.storybook/preview-head.html @@ -0,0 +1,4 @@ + + + + diff --git a/packages/frontend-react/.storybook/preview.tsx b/packages/frontend-react/.storybook/preview.tsx new file mode 100644 index 00000000..92af9bae --- /dev/null +++ b/packages/frontend-react/.storybook/preview.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +import { RouterProvider, createMemoryHistory, createRootRoute, createRouter } from '@tanstack/react-router' +import { initialize, mswLoader } from 'msw-storybook-addon' + +import type { Preview } from '@storybook/react' + +import '../src/main.css' + +initialize({ + serviceWorker: { url: '/apiMockServiceWorker.js' } +}) + +const preview: Preview = { + loaders: [mswLoader], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/ + } + } + }, + decorators: [ + (Story) => { + return ( + + ) + } + ] +} + +export default preview diff --git a/packages/frontend-react/README.md b/packages/frontend-react/README.md new file mode 100644 index 00000000..8f372f62 --- /dev/null +++ b/packages/frontend-react/README.md @@ -0,0 +1,20 @@ +# NOMOS React Frontend + +This project aims to provide a replacement frontend for the [NOMOS membership operations system](https://github.com/vhs/nomos). + +## Development + +Amongst others, this project uses the following technologies: + +- FontAwesome +- HeroIcons +- React +- React Toastify +- SWR +- Tailwind +- Tanstack (React) Router +- Typescript +- Vite +- Zod + +For more information, see the [Development Guide](./docs/Development.md) diff --git a/packages/frontend-react/bower.json b/packages/frontend-react/bower.json new file mode 100644 index 00000000..15f4577c --- /dev/null +++ b/packages/frontend-react/bower.json @@ -0,0 +1,14 @@ +{ + "name": "@vhs/nomos-frontend-react", + "description": "NOMOS React frontend", + "main": "index.html", + "authors": ["Ty Eggen "], + "license": "GPLv2", + "homepage": "https://github.com/vhs/nomos", + "private": true, + "ignore": ["**/.*", "node_modules", "bower_components", "./public/assets/", "test", "tests"], + "dependencies": { + "fontawesome": "6.7.2", + "pace": "https://github.com/HubSpot/pace.git#1.2.4" + } +} diff --git a/packages/frontend-react/conf/nginx-react-docker-compose.conf b/packages/frontend-react/conf/nginx-react-docker-compose.conf new file mode 100644 index 00000000..6116ca2b --- /dev/null +++ b/packages/frontend-react/conf/nginx-react-docker-compose.conf @@ -0,0 +1,31 @@ +server { + listen 80 default_server; + server_name _; + root /var/www/html; + error_page 404 =200 /index.html; + + location ~ (/services/|\.php$) { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_read_timeout 300; + fastcgi_pass backend-php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME /var/www/html/app/app.php; # ?service=$fastcgi_script_name; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + include fastcgi_params; + } + + location / { + index index.html; + try_files $uri $uri/ /index.html; + } + + location = /robots.txt { + return 200 "User-agent: *\nDisallow: /"; + } + + location ~* \.php$ { + include fastcgi_params; + fastcgi_pass backend-php:9000; + } +} diff --git a/packages/frontend-react/docs/Development.md b/packages/frontend-react/docs/Development.md new file mode 100644 index 00000000..5a456e13 --- /dev/null +++ b/packages/frontend-react/docs/Development.md @@ -0,0 +1,84 @@ +# Development + +## Source Code Organization + +The sources are organized in the following manner: + +- `src/components` is where all components live +- `src/lib` is where all other non-component code lives +- `src/routes` is where all routing files are kept +- `src/stories` is where all common Storybook files are kept +- `src/styles` is where all common styles files are kept +- `src/types` is where all supportive Typescript typing files reside + +Additionally, the following files are relevant: + +- `index.html` is the primary entry point for the application +- `src/main.tsx` is the primary JSX entrypoint +- `src/main.css` is the main CSS definitions file +- `src/router.tsx` provides routing scaffolding + +## Components + +### Primary Components + +This project uses a variant of the "Atomic React" pattern to organize its components. In short, Atomic React breaks up components into different types(/folders) based on re-use and complexity.
+I.e. an _atom_ is a component of bare function where as a molecule in increasingly complex, and so on. + +This project divides components based on complexity and the inclusion of other components and their respective complexity levels.
+I.e. a simple component that imports another component or external components will always at least be a Molecule. + +This project uses the following levels to achieve this: + +- _Atoms_ are re-usable, simple HTML components +- _Molecules_ are fundamental building blocks that import at least either an Atom or an external component +- _Particles_ offer more functionality (and import Molecule components) +- _Composites_ are re-usable (template) components (and import Particle components) +- _Materials_ are fully re-usable complex modules/components (and import Composite components) + +**Notes**: + +> - A good way to look at classification is that Atoms may only use plain HTML (and not import other components), and every layer beyond an Atom is determined by the highest layer imported. E.g. a Molecule component will always import at least one (1) Atom or external component, and a Particle component will at least import one (1) Molecule component. +> - External components always count as Atoms. +> - If a component has integrated sub-components, count this as one complexity layer and count from there.
E.g. if a sub-component imports a Molecule, then the main component will be a Composite level component.
(Molecule->Particle->Composite). See the Menu component as an example. +> - Every type of component can be scaffolded by using the `pnpm create:_type_` command.
+> E.g. `pnpm create:atom Logo` will create a template component by the name of `Logo` in `src/components/01-atoms/Logo`. + +### Secondary Components + +In addition to the "basic" components above, this project separates the following types of components: + +- _Layouts_ for general layout components. +- _Integrated Pages_ for common components that will adapt based on props passed. (E.g. pages in this directory can be call from an admin route with an admin prop to show the admin page instead of the user page.) +- _Pages_ divided in logical groupings. + - _Admin_ for admin page components. + - _Common_ for non-authenticated page components. + - _User_ for user page components. +- _App_ for app components. +- _Providers_ for React Content Provider components. +- _Templates_ for `generate-react-cli` template components. + +### Folder Structure + +Furthermore, all components use a numbered naming convention to enforce consistent directory ordering: + +- `src/components/00-components` +- `src/components/01-atoms` +- `src/components/02-molecules` +- `src/components/03-particles` +- `src/components/04-composites` +- `src/components/05-materials` +- `src/components/06-layouts` +- `src/components/07-integrated-pages` +- `src/components/07-pages` +- `src/components/08-app` +- `src/components/09-providers` +- `src/components/99-templates` + +**Note**: + +> While new components may be created as a `component` and moved to the appropriate directory later, but components should never be committed to the `00-components` directory. + +## Routing + +The basic page/route workflow consists of a `route` file (that extends a particular `layout`) which calls a `page` that imports all the relevant components. diff --git a/packages/frontend-react/eslint.config.mjs b/packages/frontend-react/eslint.config.mjs new file mode 100644 index 00000000..5482acf9 --- /dev/null +++ b/packages/frontend-react/eslint.config.mjs @@ -0,0 +1,100 @@ +import pluginRouter from '@tanstack/eslint-plugin-router' +import cjs from '@tyisi/config-eslint/cjs' +import js from '@tyisi/config-eslint/js' +import mjs from '@tyisi/config-eslint/mjs' +import ts from '@tyisi/config-eslint/ts' +import tsx from '@tyisi/config-eslint/tsx' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import pluginReactRefresh from 'eslint-plugin-react-refresh' +import pluginStorybook from 'eslint-plugin-storybook' +import unusedImports from 'eslint-plugin-unused-imports' +import globals from 'globals' + +function hoistConfig(config, withReact) { + withReact ??= false + + config[0].languageOptions.parserOptions = { + ecmaVersion: 'latest', + sourceType: 'module', + projectService: { + allowDefaultProject: ['src/', '.storybook/*.ts', '.storybook/*.tsx'] + }, + // @ts-ignore + tsconfigRootDir: process.cwd(), + ecmaFeatures: { + jsx: withReact + } + } + + if (withReact) config[0].languageOptions.globals = { ...config[0].languageOptions.globals, ...globals.browser } + + config[0].languageOptions.globals.describe = false + config[0].languageOptions.globals.it = false + + config[0].languageOptions.globals.Highcharts = false + + config[0].languageOptions.globals.top = false + + if (withReact) config[0].plugins['react-hooks'] = pluginReactHooks + if (withReact) config[0].plugins['react-refresh'] = pluginReactRefresh + if (withReact) config[0].plugins['@tanstack/router'] = pluginRouter + + if (withReact) config[0].rules['react-refresh/only-export-components'] = ['warn', { allowConstantExport: true }] + + if (withReact) + // @ts-ignore + Object.entries(pluginRouter.configs['flat/recommended'][0].rules).forEach(([k, v]) => (config[0].rules[k] = v)) + if (withReact) + Object.entries(pluginReactHooks.configs.recommended.rules).forEach(([k, v]) => (config[0].rules[k] = v)) + if (withReact) + Object.entries(pluginReactHooks.configs.recommended.rules).forEach(([k, v]) => (config[0].rules[k] = v)) + + delete config[0].rules['@typescript-eslint/no-unnecessary-condition'] + + if (withReact) { + config[0].plugins['storybook'] = pluginStorybook + + pluginStorybook.configs['flat/recommended'].forEach((storybookConfigSlice) => { + const newConfigSlice = {} + + if (storybookConfigSlice.files != null) { + newConfigSlice.files = [] + storybookConfigSlice.files.forEach((e) => newConfigSlice.files.push(e)) + } + + if (storybookConfigSlice.rules != null) { + newConfigSlice.rules = { ...config[0].rules } + Object.entries(storybookConfigSlice.rules).forEach(([k, v]) => (newConfigSlice.rules[k] = v)) + } + + config.push({ ...config[0], ...newConfigSlice }) + }) + } + + config[0].plugins['unused-imports'] = unusedImports + config[0].rules['unused-imports/no-unused-imports'] = 'warn' + + config[0].rules['max-params'] = 'off' + config[0].rules['@typescript-eslint/max-params'] = ['error', { max: 12 }] + + config[0].rules['@typescript-eslint/triple-slash-reference'] = [ + 'error', + { lib: 'always', path: 'always', types: 'always' } + ] + + if (withReact) + config[0].rules['jsx-a11y/label-has-associated-control'] = [ + 'error', + { + controlComponents: ['FormControl'], + depth: 3 + } + ] + + config.unshift({ ignores: ['src/routeTree.gen.ts', 'src/types/nomos.d.ts'] }) +} + +hoistConfig(ts, false) +hoistConfig(tsx, true) + +export default [...cjs, ...mjs, ...js, ...ts, ...tsx] diff --git a/packages/frontend-react/generate-react-cli.json b/packages/frontend-react/generate-react-cli.json new file mode 100644 index 00000000..daef5383 --- /dev/null +++ b/packages/frontend-react/generate-react-cli.json @@ -0,0 +1,280 @@ +{ + "usesTypeScript": true, + "usesStyledComponents": false, + "usesCssModule": true, + "cssPreprocessor": "scss", + "testLibrary": "Testing Library", + "component": { + "default": { + "path": "src/components/00-components", + "withLazy": false, + "withStory": true, + "withStyle": false, + "withTest": false, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "atom": { + "path": "src/components/01-atoms", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": false, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "molecule": { + "path": "src/components/02-molecules", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "particle": { + "path": "src/components/03-particles", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "composite": { + "path": "src/components/04-composites", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "material": { + "path": "src/components/05-materials", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "layout": { + "path": "src/components/06-layouts", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "page": { + "path": "src/components/07-pages", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "page:admin": { + "path": "src/components/07-pages/admin", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customDirectory": "AdminTemplateName", + "customTemplates": { + "component": "src/components/99-templates/admin-page/AdminTemplateName.tsx", + "lazy": "src/components/99-templates/admin-page/AdminTemplateName.lazy.tsx", + "story": "src/components/99-templates/admin-page/AdminTemplateName.stories.tsx", + "style": "src/components/99-templates/admin-page/AdminTemplateName.module.css", + "test": "src/components/99-templates/admin-page/AdminTemplateName.test.tsx", + "types": "src/components/99-templates/admin-page/AdminTemplateName.types.ts", + "utils": "src/components/99-templates/admin-page/AdminTemplateName.utils.ts" + } + }, + "page:admin:item": { + "path": "src/components/07-pages/admin", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": false, + "withTypes": true, + "withUtils": true, + "customDirectory": "AdminTemplateName/item", + "customTemplates": { + "component": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.tsx", + "lazy": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.lazy.tsx", + "story": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.stories.tsx", + "test": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.test.ts", + "types": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.types.ts", + "utils": "src/components/99-templates/admin-page/item/AdminTemplateNameItem.utils.ts" + } + }, + "page:common": { + "path": "src/components/07-pages/common", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/page/TemplateName.tsx", + "lazy": "src/components/99-templates/page/TemplateName.lazy.tsx", + "story": "src/components/99-templates/page/TemplateName.stories.tsx", + "style": "src/components/99-templates/page/TemplateName.module.css", + "test": "src/components/99-templates/page/TemplateName.test.tsx", + "types": "src/components/99-templates/page/TemplateName.types.ts", + "utils": "src/components/99-templates/page/TemplateName.utils.ts" + } + }, + "page:public": { + "path": "src/components/07-pages/public", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/page/TemplateName.tsx", + "lazy": "src/components/99-templates/page/TemplateName.lazy.tsx", + "story": "src/components/99-templates/page/TemplateName.stories.tsx", + "style": "src/components/99-templates/page/TemplateName.module.css", + "test": "src/components/99-templates/page/TemplateName.test.tsx", + "types": "src/components/99-templates/page/TemplateName.types.ts", + "utils": "src/components/99-templates/page/TemplateName.utils.ts" + } + }, + "page:user": { + "path": "src/components/07-pages/user", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/page/TemplateName.tsx", + "lazy": "src/components/99-templates/page/TemplateName.lazy.tsx", + "story": "src/components/99-templates/page/TemplateName.stories.tsx", + "style": "src/components/99-templates/page/TemplateName.module.css", + "test": "src/components/99-templates/page/TemplateName.test.tsx", + "types": "src/components/99-templates/page/TemplateName.types.ts", + "utils": "src/components/99-templates/page/TemplateName.utils.ts" + } + }, + "app": { + "path": "src/components/08-app", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + }, + "provider": { + "path": "src/components/09-providers", + "withLazy": true, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withUtils": true, + "customTemplates": { + "component": "src/components/99-templates/default/TemplateName.tsx", + "lazy": "src/components/99-templates/default/TemplateName.lazy.tsx", + "story": "src/components/99-templates/default/TemplateName.stories.tsx", + "style": "src/components/99-templates/default/TemplateName.module.css", + "test": "src/components/99-templates/default/TemplateName.test.tsx", + "types": "src/components/99-templates/default/TemplateName.types.ts", + "utils": "src/components/99-templates/default/TemplateName.utils.ts" + } + } + } +} diff --git a/packages/frontend-react/index.html b/packages/frontend-react/index.html new file mode 100644 index 00000000..5d779e67 --- /dev/null +++ b/packages/frontend-react/index.html @@ -0,0 +1,20 @@ + + + + + + + + NOMOS + + + + + + + + +
+ + + diff --git a/packages/frontend-react/package.json b/packages/frontend-react/package.json new file mode 100644 index 00000000..32f31ca8 --- /dev/null +++ b/packages/frontend-react/package.json @@ -0,0 +1,214 @@ +{ + "dependencies": { + "@heroicons/react": "2.2.0", + "@hookform/resolvers": "^4.1.0", + "@tanstack/react-router": "1.100.0", + "@vitejs/plugin-react": "^4.3.4", + "anderm-react-timeago": "4.4.2", + "chart.js": "4.4.7", + "clsx": "2.1.1", + "crypto-js": "4.2.0", + "javascript-color-gradient": "2.5.0", + "just-install": "2.0.2", + "moment": "2.30.1", + "react": "18.3.1", + "react-chartjs-2": "5.3.0", + "react-dom": "18.3.1", + "react-gravatar": "2.6.3", + "react-hook-form": "^7.54.2", + "react-spinners": "0.15.0", + "react-toastify": "11.0.3", + "swr": "2.3.2", + "wireit": "0.14.12", + "zod": "3.24.1" + }, + "devDependencies": { + "@storybook/addon-docs": "^8.5.5", + "@storybook/addon-essentials": "^8.5.5", + "@storybook/addon-interactions": "^8.5.5", + "@storybook/addon-links": "^8.5.5", + "@storybook/blocks": "^8.5.5", + "@storybook/csf": "^0.1.13", + "@storybook/react": "^8.5.5", + "@storybook/react-vite": "^8.5.5", + "@storybook/test": "^8.5.5", + "@tanstack/eslint-plugin-router": "1.99.3", + "@tanstack/router-devtools": "1.100.0", + "@tanstack/router-plugin": "1.100.0", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", + "@tyisi/config-eslint": "4.0.0", + "@tyisi/config-prettier": "1.0.1", + "@tyisi/config-stylelint": "1.1.0", + "@types/crypto-js": "4.2.2", + "@types/javascript-color-gradient": "2.4.2", + "@types/jest": "29.5.14", + "@types/node": "22.13.1", + "@types/react": "19.0.8", + "@types/react-dom": "19.0.3", + "@types/react-gravatar": "2.6.14", + "@vitejs/plugin-basic-ssl": "1.2.0", + "@vitejs/plugin-react-swc": "3.7.2", + "autoprefixer": "^10.4.20", + "bower": "^1.8.14", + "eslint": "9.20.0", + "eslint-plugin-jest": "28.9.0", + "eslint-plugin-react-hooks": "5.1.0", + "eslint-plugin-react-refresh": "0.4.18", + "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-unused-imports": "^4.1.4", + "generate-react-cli": "8.4.9", + "globals": "15.14.0", + "husky": "9.1.7", + "jest": "29.7.0", + "msw": "2.7.0", + "msw-storybook-addon": "2.0.4", + "postcss": "8.5.1", + "postcss-import": "^16.1.0", + "prettier": "3.4.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "sass": "1.84.0", + "storybook": "^8.5.5", + "stylelint": "16.14.1", + "tailwindcss": "3.4.17", + "typescript": "5.7.3", + "vite": "6.1.0", + "vite-tsconfig-paths": "5.1.4" + }, + "msw": { + "workerDirectory": [ + "public" + ] + }, + "name": "@vhs/nomos-frontend-react", + "private": true, + "scripts": { + "build": "wireit", + "build:storybook": "wireit", + "clean:dist": "wireit", + "create:atom": "generate-react c --type=atom", + "create:component": "generate-react c --type=component", + "create:composite": "generate-react c --type=composite", + "create:layout": "generate-react c --type=layout", + "create:material": "generate-react c --type=material", + "create:molecule": "generate-react c --type=molecule", + "create:page": "generate-react c --type=page", + "create:page:admin": "generate-react c --type=page:admin", + "create:page:admin:item": "generate-react c --type=page:admin:item", + "create:page:common": "generate-react c --type=page:common", + "create:page:public": "generate-react c --type=page:public", + "create:page:user": "generate-react c --type=page:user", + "create:particle": "generate-react c --type=particle", + "create:provider": "generate-react c --type=provider", + "dev": "vite", + "dev:lan": "vite --host", + "fix:storybook:titles": "wireit", + "generate:fontawesome": "wireit", + "generate:fontawesome:names": "wireit", + "generate:fontawesome:types": "wireit", + "generate:validator:implementations": "wireit", + "install:dependencies": "wireit", + "lint": "wireit", + "prepare": "wireit", + "preview": "vite preview", + "storybook": "wireit", + "type-check": "wireit" + }, + "type": "module", + "version": "0.0.0", + "wireit": { + "build": { + "command": "vite build", + "dependencies": [ + "install:dependencies", + "generate:fontawesome", + "compile", + "clean:build:assets" + ] + }, + "build:storybook": { + "command": "storybook build" + }, + "clean:bower:assets": { + "command": "rm -fR ./public/assets/*" + }, + "clean:dist": { + "command": "rm -fR dist/*" + }, + "clean:build:assets": { + "command": "tools/clean-build-assets.sh", + "dependencies": [ + "install:dependencies", + "compile", + "generate:fontawesome" + ] + }, + "compile": { + "command": "tsc", + "dependencies": [ + "install:dependencies", + "generate:fontawesome" + ] + }, + "fix:storybook:titles": { + "command": "./tools/fix-storybook-titles.sh" + }, + "generate:fontawesome": { + "dependencies": [ + "install:dependencies", + "generate:fontawesome:names", + "generate:fontawesome:types" + ] + }, + "generate:fontawesome:names": { + "command": "./tools/generate-fa-names.sh", + "dependencies": [ + "install:dependencies" + ] + }, + "generate:fontawesome:types": { + "command": "./tools/generate-fa-types.sh", + "dependencies": [ + "install:dependencies" + ] + }, + "generate:validator:implementations": { + "command": "./tools/generate-validator-implementations.sh" + }, + "install:bower": { + "command": "bower install --force", + "files": [ + "public/assets/**" + ], + "clean": true + }, + "install:config": { + "command": "./tools/bootstrap-config.sh" + }, + "install:dependencies": { + "dependencies": [ + "install:bower", + "install:config" + ] + }, + "lint": { + "command": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "prepare": { + "dependencies": [ + "install:dependencies" + ] + }, + "storybook": { + "command": "storybook dev -p 6006 --https --ssl-ca ./certs/ca.crt --ssl-key ./certs/localhost.key --ssl-cert ./certs/localhost.crt", + "dependencies": [ + "fix:storybook:titles" + ], + "service": true + }, + "type-check": { + "command": "tsc --noEmit" + } + } +} diff --git a/packages/frontend-react/postcss.config.js b/packages/frontend-react/postcss.config.js new file mode 100644 index 00000000..801a9de0 --- /dev/null +++ b/packages/frontend-react/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss': {}, + 'autoprefixer': {} + } +} diff --git a/packages/frontend-react/prettier.config.mjs b/packages/frontend-react/prettier.config.mjs new file mode 100644 index 00000000..15e3ea33 --- /dev/null +++ b/packages/frontend-react/prettier.config.mjs @@ -0,0 +1,5 @@ +import config from '../../prettier.config.mjs' + +config.printWidth = 120 + +export default config diff --git a/packages/frontend-react/public/apiMockServiceWorker.js b/packages/frontend-react/public/apiMockServiceWorker.js new file mode 100644 index 00000000..9b25f6da --- /dev/null +++ b/packages/frontend-react/public/apiMockServiceWorker.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line no-undef +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url) + + if (!url.pathname.startsWith('/services/')) { + // Do not propagate this event to other listeners (from MSW) + event.stopImmediatePropagation() + } +}) + +// eslint-disable-next-line no-undef +importScripts('./mockServiceWorker.js') diff --git a/web/badges/cert_cnc_mill_lathe.svg b/packages/frontend-react/public/badges/cert_cnc_mill_lathe.svg similarity index 100% rename from web/badges/cert_cnc_mill_lathe.svg rename to packages/frontend-react/public/badges/cert_cnc_mill_lathe.svg diff --git a/web/badges/cert_laser.svg b/packages/frontend-react/public/badges/cert_laser.svg similarity index 100% rename from web/badges/cert_laser.svg rename to packages/frontend-react/public/badges/cert_laser.svg diff --git a/web/badges/door_access.svg b/packages/frontend-react/public/badges/door_access.svg similarity index 100% rename from web/badges/door_access.svg rename to packages/frontend-react/public/badges/door_access.svg diff --git a/web/badges/key_holder.svg b/packages/frontend-react/public/badges/key_holder.svg similarity index 100% rename from web/badges/key_holder.svg rename to packages/frontend-react/public/badges/key_holder.svg diff --git a/web/badges/key_holder_inactive.svg b/packages/frontend-react/public/badges/key_holder_inactive.svg similarity index 100% rename from web/badges/key_holder_inactive.svg rename to packages/frontend-react/public/badges/key_holder_inactive.svg diff --git a/web/badges/laser.png b/packages/frontend-react/public/badges/laser.png similarity index 100% rename from web/badges/laser.png rename to packages/frontend-react/public/badges/laser.png diff --git a/web/badges/newsletter.svg b/packages/frontend-react/public/badges/newsletter.svg similarity index 100% rename from web/badges/newsletter.svg rename to packages/frontend-react/public/badges/newsletter.svg diff --git a/packages/frontend-react/public/custom/pace.local.css b/packages/frontend-react/public/custom/pace.local.css new file mode 100644 index 00000000..4102bb03 --- /dev/null +++ b/packages/frontend-react/public/custom/pace.local.css @@ -0,0 +1,22 @@ +.pace { + -webkit-pointer-events: none; + pointer-events: none; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background: #2563eb; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 2px; +} diff --git a/web/favicon.ico b/packages/frontend-react/public/favicon.ico similarity index 100% rename from web/favicon.ico rename to packages/frontend-react/public/favicon.ico diff --git a/web/images/logo.png b/packages/frontend-react/public/images/logo.png similarity index 100% rename from web/images/logo.png rename to packages/frontend-react/public/images/logo.png diff --git a/web/images/provider/github.png b/packages/frontend-react/public/images/provider/github.png similarity index 100% rename from web/images/provider/github.png rename to packages/frontend-react/public/images/provider/github.png diff --git a/web/images/provider/google.png b/packages/frontend-react/public/images/provider/google.png similarity index 100% rename from web/images/provider/google.png rename to packages/frontend-react/public/images/provider/google.png diff --git a/web/images/provider/pin.png b/packages/frontend-react/public/images/provider/pin.png similarity index 100% rename from web/images/provider/pin.png rename to packages/frontend-react/public/images/provider/pin.png diff --git a/web/images/provider/rfid.png b/packages/frontend-react/public/images/provider/rfid.png similarity index 100% rename from web/images/provider/rfid.png rename to packages/frontend-react/public/images/provider/rfid.png diff --git a/web/images/provider/slack.png b/packages/frontend-react/public/images/provider/slack.png similarity index 100% rename from web/images/provider/slack.png rename to packages/frontend-react/public/images/provider/slack.png diff --git a/web/images/slack.png b/packages/frontend-react/public/images/slack.png similarity index 100% rename from web/images/slack.png rename to packages/frontend-react/public/images/slack.png diff --git a/packages/frontend-react/public/images/under-construction90s-90s.gif b/packages/frontend-react/public/images/under-construction90s-90s.gif new file mode 100644 index 00000000..ab87cfc5 Binary files /dev/null and b/packages/frontend-react/public/images/under-construction90s-90s.gif differ diff --git a/web/images/vhs-member-card-2015-2-full.png b/packages/frontend-react/public/images/vhs-member-card-2015-2-full.png similarity index 100% rename from web/images/vhs-member-card-2015-2-full.png rename to packages/frontend-react/public/images/vhs-member-card-2015-2-full.png diff --git a/web/images/vhs-member-card-2015-2-thumb.png b/packages/frontend-react/public/images/vhs-member-card-2015-2-thumb.png similarity index 100% rename from web/images/vhs-member-card-2015-2-thumb.png rename to packages/frontend-react/public/images/vhs-member-card-2015-2-thumb.png diff --git a/web/images/vhs-member-card-2015-full.png b/packages/frontend-react/public/images/vhs-member-card-2015-full.png similarity index 100% rename from web/images/vhs-member-card-2015-full.png rename to packages/frontend-react/public/images/vhs-member-card-2015-full.png diff --git a/web/images/vhs-member-card-2015-thumb.png b/packages/frontend-react/public/images/vhs-member-card-2015-thumb.png similarity index 100% rename from web/images/vhs-member-card-2015-thumb.png rename to packages/frontend-react/public/images/vhs-member-card-2015-thumb.png diff --git a/packages/frontend-react/public/mockServiceWorker.js b/packages/frontend-react/public/mockServiceWorker.js new file mode 100644 index 00000000..193778d7 --- /dev/null +++ b/packages/frontend-react/public/mockServiceWorker.js @@ -0,0 +1,302 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.7.0' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window' + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE' + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM + } + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType + } + } + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()) + } + }, + [responseClone.body] + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window' + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter((value) => value !== 'msw/passthrough') + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive + } + }, + [requestBuffer] + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean))) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true + }) + + return mockedResponse +} diff --git a/packages/frontend-react/public/robots.txt b/packages/frontend-react/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/packages/frontend-react/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/packages/frontend-react/skel/config.json b/packages/frontend-react/skel/config.json new file mode 100644 index 00000000..8c1fa02d --- /dev/null +++ b/packages/frontend-react/skel/config.json @@ -0,0 +1,3 @@ +{ + "baseApiUri": "" +} diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Col/Col.lazy.tsx new file mode 100644 index 00000000..15bb99fd --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ColProps } from './Col.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCol = lazy(async () => await import('./Col')) + +const Col = (props: JSX.IntrinsicAttributes & ColProps): JSX.Element => ( + }> + + +) + +export default Col diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.stories.tsx b/packages/frontend-react/src/components/01-atoms/Col/Col.stories.tsx new file mode 100644 index 00000000..b25e1e1c --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Col from './Col' + +type StoryType = StoryObj + +const meta: Meta = { + component: Col, + title: '01-Atoms/Col', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Col' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.tsx b/packages/frontend-react/src/components/01-atoms/Col/Col.tsx new file mode 100644 index 00000000..4f4138eb --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.tsx @@ -0,0 +1,34 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { ColBreakPoint, ColProps } from './Col.types' + +const breakpoints: ColBreakPoint[] = ['default', 'xs', 'sm', 'md', 'lg', 'xl'] + +const Col: FC = ({ children, className, ...restProps }) => { + const breakpointClasses = breakpoints + .filter((breakpoint) => restProps[breakpoint] != null) + .map((breakpoint) => { + const basis = + restProps[breakpoint]?.toString() === '12' + ? `${breakpoint}:basis-full` + : `${breakpoint}:basis-${restProps[breakpoint]}/12` + + return basis + }) + + const classNames = [className, ...breakpointClasses, 'col'].map((cn) => cn?.replace(/default:/, '')) + + if (!classNames.join(' ').includes('basis-')) { + classNames.splice(1, 0, 'shrink grow basis-0') + } + + return ( +
+ {children} +
+ ) +} + +export default Col diff --git a/packages/frontend-react/src/components/01-atoms/Col/Col.types.ts b/packages/frontend-react/src/components/01-atoms/Col/Col.types.ts new file mode 100644 index 00000000..563e5208 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Col/Col.types.ts @@ -0,0 +1,11 @@ +import type { MouseEventHandler, ReactNode } from 'react' + +export type ColBreakPoint = 'default' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +export type ColBreakPointRecord = Partial> + +export interface ColProps extends ColBreakPointRecord { + children?: ReactNode + className?: string + onClick?: MouseEventHandler +} diff --git a/web/user/home/home.html b/packages/frontend-react/src/components/01-atoms/Col/Col.utils.ts similarity index 100% rename from web/user/home/home.html rename to packages/frontend-react/src/components/01-atoms/Col/Col.utils.ts diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.stories.tsx b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.stories.tsx new file mode 100644 index 00000000..5974a181 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.stories.tsx @@ -0,0 +1,18 @@ +import type { JSX } from 'react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import Conditional from './Conditional' + +export default { + title: '01-Atoms/Conditional', + decorators: [CenteredContentStorybookDecorator] +} + +export const Disabled = (): JSX.Element => Disabled + +Disabled.storyName = 'disabled' + +export const Enabled = (): JSX.Element => Enabled + +Enabled.storyName = 'enabled' diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.tsx b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.tsx new file mode 100644 index 00000000..fade25cd --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.tsx @@ -0,0 +1,12 @@ +import type { FC } from 'react' + +import type { ConditionalProps } from './Conditional.types' + +// eslint-disable-next-line @typescript-eslint/promise-function-async +const Conditional: FC = ({ condition, fallback, children }) => { + if (!condition) return fallback ?? null + + return <>{children} +} + +export default Conditional diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.types.ts b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.types.ts new file mode 100644 index 00000000..38a1a6b1 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface ConditionalProps { + condition: boolean + fallback?: ReactNode + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.utils.ts b/packages/frontend-react/src/components/01-atoms/Conditional/Conditional.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Container/Container.lazy.tsx new file mode 100644 index 00000000..3565def1 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ContainerProps } from './Container.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyContainer = lazy(async () => await import('./Container')) + +const Container = (props: JSX.IntrinsicAttributes & ContainerProps): JSX.Element => ( + }> + + +) + +export default Container diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.stories.tsx b/packages/frontend-react/src/components/01-atoms/Container/Container.stories.tsx new file mode 100644 index 00000000..ba6ffb94 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import Container from './Container' + +type StoryType = StoryObj + +const meta: Meta = { + component: Container, + title: '01-Atoms/Container', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Container' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.tsx b/packages/frontend-react/src/components/01-atoms/Container/Container.tsx new file mode 100644 index 00000000..a49070e7 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.tsx @@ -0,0 +1,17 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { ContainerProps } from './Container.types' + +const Container: FC = ({ fluid, className, children }) => { + fluid ??= false + + return ( +
+ {children} +
+ ) +} + +export default Container diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.types.ts b/packages/frontend-react/src/components/01-atoms/Container/Container.types.ts new file mode 100644 index 00000000..bae65014 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Container/Container.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface ContainerProps { + children?: ReactNode + fluid?: boolean + className?: string +} diff --git a/packages/frontend-react/src/components/01-atoms/Container/Container.utils.ts b/packages/frontend-react/src/components/01-atoms/Container/Container.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.lazy.tsx b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.lazy.tsx new file mode 100644 index 00000000..7ca330f1 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FontAwesomeIconProps } from './FontAwesomeIcon.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFontAwesomeIcon = lazy(async () => await import('./FontAwesomeIcon')) + +const FontAwesomeIcon = (props: JSX.IntrinsicAttributes & FontAwesomeIconProps): JSX.Element => ( + }> + + +) + +export default FontAwesomeIcon diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.stories.tsx b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.stories.tsx new file mode 100644 index 00000000..ee2280ce --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import FontAwesomeIcon from './FontAwesomeIcon' + +type StoryType = StoryObj + +const meta: Meta = { + component: FontAwesomeIcon, + title: '01-Atoms/FontAwesomeIcon', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + category: 'brand', + icon: 'facebook', + className: 'text-blue-500', + size: '5x' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.tsx b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.tsx new file mode 100644 index 00000000..fe113eb5 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.tsx @@ -0,0 +1,47 @@ +import { useMemo, type FC } from 'react' + +import { clsx } from 'clsx' + +import type { FontAwesomeIconProps } from './FontAwesomeIcon.types' + +const FontAwesomeIcon: FC = ({ + category, + className, + effect, + flip, + icon, + inverse, + pullLeft, + pullRight, + rotate, + size, + stack, + ...restProps +}) => { + const iconClassName = useMemo(() => { + const iconOptions = [] + + iconOptions.push(`fa${(category ?? 'solid')[0]}`) + + const iconArray = Array.isArray(icon) ? icon : [icon] + iconArray.forEach((i) => iconOptions.push(`fa-${i}`)) + + if (effect != null) { + const effectArray = Array.isArray(effect) ? effect : [effect] + effectArray.forEach((e) => iconOptions.push(`fa-${e}`)) + } + if (inverse != null) iconOptions.push(`fa-inverse`) + if (pullLeft != null) iconOptions.push(`fa-pull-left`) + if (pullRight != null) iconOptions.push(`fa-pull-right`) + if (flip != null) iconOptions.push(`fa-${flip}`) + if (size != null) iconOptions.push(`fa-${size}`) + if (rotate != null) iconOptions.push(`fa-${rotate}`) + if (stack != null) iconOptions.push(`fa-${stack === true ? 'stack' : stack}`) + + return iconOptions.join(' ') + }, [icon, category, effect, flip, inverse, pullLeft, pullRight, rotate, size, stack]) + + return +} + +export default FontAwesomeIcon diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types.ts b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types.ts new file mode 100644 index 00000000..a0e65f5c --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types.ts @@ -0,0 +1,27 @@ +import type { + FAB, + FAS, + FontAwesomeCategoryOption, + FontAwesomeEffectOption, + FontAwesomeFlipOption, + FontAwesomeRotateOption, + FontAwesomeSizeOption, + FontAwesomeStackOption +} from '@/types/fontawesome' +import type { CastReactElement, SingleOrArray } from '@/types/utils' + +export type IconProp = FAB | FAS | string[] + +export interface FontAwesomeIconProps extends Partial> { + icon: IconProp + category?: FontAwesomeCategoryOption + className?: string + effect?: SingleOrArray + flip?: FontAwesomeFlipOption + inverse?: boolean + pullLeft?: boolean + pullRight?: boolean + rotate?: FontAwesomeRotateOption + size?: FontAwesomeSizeOption + stack?: FontAwesomeStackOption +} diff --git a/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.utils.ts b/packages/frontend-react/src/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.lazy.tsx b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.lazy.tsx new file mode 100644 index 00000000..bb7c0214 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { NavBarProps } from './NavBar.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyNavBar = lazy(async () => await import('./NavBar')) + +const NavBar = (props: JSX.IntrinsicAttributes & NavBarProps): JSX.Element => ( + }> + + +) + +export default NavBar diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.stories.tsx b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.stories.tsx new file mode 100644 index 00000000..32fb214b --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import NavBar from './NavBar' + +type StoryType = StoryObj + +const meta: Meta = { + component: NavBar, + title: '01-Atoms/NavBar', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'NavBar' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.tsx b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.tsx new file mode 100644 index 00000000..b7d1890e --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.tsx @@ -0,0 +1,14 @@ +import type { FC } from 'react' + +import type { NavBarProps } from './NavBar.types' + +const NavBar: FC = ({ children }) => ( +
+ {children} +
+) + +export default NavBar diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.types.ts b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.types.ts new file mode 100644 index 00000000..9c211c84 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface NavBarProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.utils.ts b/packages/frontend-react/src/components/01-atoms/NavBar/NavBar.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.lazy.tsx new file mode 100644 index 00000000..da920ba7 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { OverlayProps } from './Overlay.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyOverlay = lazy(async () => await import('./Overlay')) + +const Overlay = (props: JSX.IntrinsicAttributes & OverlayProps): JSX.Element => ( + }> + + +) + +export default Overlay diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.stories.tsx b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.stories.tsx new file mode 100644 index 00000000..09a31820 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Overlay from './Overlay' + +type StoryType = StoryObj + +const meta: Meta = { + component: Overlay, + title: '01-Atoms/Overlay', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Overlay' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.tsx b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.tsx new file mode 100644 index 00000000..db339751 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.tsx @@ -0,0 +1,31 @@ +import { useCallback, useEffect, type FC } from 'react' + +import type { OverlayProps } from './Overlay.types' + +import useOutsideClick from '@/lib/hooks/useClickOutside' + +const Overlay: FC = ({ children, handler }) => { + const clickHandler = useCallback(() => { + if (handler != null) handler(false) + }, [handler]) + + const ref = useOutsideClick(clickHandler) + + useEffect(() => { + ;(document.getElementsByTagName('HTML')[0] as HTMLElement).style.overflow = 'hidden' + + return () => { + ;(document.getElementsByTagName('HTML')[0] as HTMLElement).style.overflow = 'auto' + } + }, []) + + return ( +
+
+ {children} +
+
+ ) +} + +export default Overlay diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.types.ts b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.types.ts new file mode 100644 index 00000000..f49660db --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { ReactAction } from '@/types/ui' + +export interface OverlayProps { + children?: ReactNode + handler?: ReactAction +} diff --git a/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.utils.ts b/packages/frontend-react/src/components/01-atoms/Overlay/Overlay.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Pill/Pill.lazy.tsx new file mode 100644 index 00000000..8d557f16 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { PillProps } from './Pill.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyPill = lazy(async () => await import('./Pill')) + +const Pill = (props: JSX.IntrinsicAttributes & PillProps): JSX.Element => ( + }> + + +) + +export default Pill diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.stories.tsx b/packages/frontend-react/src/components/01-atoms/Pill/Pill.stories.tsx new file mode 100644 index 00000000..9e7d3e27 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import Pill from './Pill' + +type StoryType = StoryObj + +const meta: Meta = { + component: Pill, + title: '01-Atoms/Pill', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Pill' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.tsx b/packages/frontend-react/src/components/01-atoms/Pill/Pill.tsx new file mode 100644 index 00000000..bf823d24 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.tsx @@ -0,0 +1,19 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { PillProps } from './Pill.types' + +const Pill: FC = ({ children, className }) => ( +
+ {children} +
+) + +export default Pill diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.types.ts b/packages/frontend-react/src/components/01-atoms/Pill/Pill.types.ts new file mode 100644 index 00000000..d93fabaa --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Pill/Pill.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface PillProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/01-atoms/Pill/Pill.utils.ts b/packages/frontend-react/src/components/01-atoms/Pill/Pill.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Popover/Popover.lazy.tsx new file mode 100644 index 00000000..713b83de --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { PopoverProps } from './Popover.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyPopover = lazy(async () => await import('./Popover')) + +const Popover = (props: JSX.IntrinsicAttributes & PopoverProps): JSX.Element => ( + }> + + +) + +export default Popover diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.stories.tsx b/packages/frontend-react/src/components/01-atoms/Popover/Popover.stories.tsx new file mode 100644 index 00000000..bf553651 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import Popover from './Popover' + +type StoryType = StoryObj + +const meta: Meta = { + component: Popover, + title: '01-Atoms/Popover', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + content: 'Popover', + popover: 'Popover Content' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.tsx b/packages/frontend-react/src/components/01-atoms/Popover/Popover.tsx new file mode 100644 index 00000000..ff3d02c3 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.tsx @@ -0,0 +1,20 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { PopoverProps } from './Popover.types' + +// TODO make popover a clickable toggle to persist popover for easy interaction + +const Popover: FC = ({ className, content, popover }) => ( +
+
{content}
+
+
+ {popover} +
+
+
+) + +export default Popover diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.types.ts b/packages/frontend-react/src/components/01-atoms/Popover/Popover.types.ts new file mode 100644 index 00000000..458ddd87 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Popover/Popover.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface PopoverProps { + className?: string + content: ReactNode + popover: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/Popover/Popover.utils.ts b/packages/frontend-react/src/components/01-atoms/Popover/Popover.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.lazy.tsx b/packages/frontend-react/src/components/01-atoms/Row/Row.lazy.tsx new file mode 100644 index 00000000..0df366f0 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RowProps } from './Row.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRow = lazy(async () => await import('./Row')) + +const Row = (props: JSX.IntrinsicAttributes & RowProps): JSX.Element => ( + }> + + +) + +export default Row diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.stories.tsx b/packages/frontend-react/src/components/01-atoms/Row/Row.stories.tsx new file mode 100644 index 00000000..459c147d --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Row from './Row' + +type StoryType = StoryObj + +const meta: Meta = { + component: Row, + title: '01-Atoms/Row', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Row' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.tsx b/packages/frontend-react/src/components/01-atoms/Row/Row.tsx new file mode 100644 index 00000000..d0f74e66 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { RowProps } from './Row.types' + +const Row: FC = ({ children, className, noWrap, ...restProps }) => { + noWrap ??= false + + return ( +
+ {children} +
+ ) +} + +export default Row diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.types.ts b/packages/frontend-react/src/components/01-atoms/Row/Row.types.ts new file mode 100644 index 00000000..de78a270 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/Row/Row.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { CastReactElement } from '@/types/utils' + +export interface RowProps extends CastReactElement<'div'> { + children?: ReactNode + noWrap?: boolean +} diff --git a/packages/frontend-react/src/components/01-atoms/Row/Row.utils.ts b/packages/frontend-react/src/components/01-atoms/Row/Row.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.lazy.tsx b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.lazy.tsx new file mode 100644 index 00000000..520cee13 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { TableActionsCellProps } from './TableActionsCell.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyTableActionsCell = lazy(async () => await import('./TableActionsCell')) + +const TableActionsCell = (props: JSX.IntrinsicAttributes & TableActionsCellProps): JSX.Element => ( + }> + + +) + +export default TableActionsCell diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.stories.tsx b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.stories.tsx new file mode 100644 index 00000000..9691ef8d --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import TableActionsCell from './TableActionsCell' + +type StoryType = StoryObj + +const meta: Meta = { + component: TableActionsCell, + title: '01-Atoms/TableActionsCell', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'TableActionsCell' + } +} diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.tsx b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.tsx new file mode 100644 index 00000000..ccc263d1 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.tsx @@ -0,0 +1,11 @@ +import type { FC } from 'react' + +import type { TableActionsCellProps } from './TableActionsCell.types' + +const TableActionsCell: FC = ({ className, children }) => ( + +
{children}
+ +) + +export default TableActionsCell diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.types.ts b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.types.ts new file mode 100644 index 00000000..9944eb72 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface TableActionsCellProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.utils.ts b/packages/frontend-react/src/components/01-atoms/TableActionsCell/TableActionsCell.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.lazy.tsx b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.lazy.tsx new file mode 100644 index 00000000..0ab182dd --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { TablePageRowProps } from './TablePageRow.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyTablePageRow = lazy(async () => await import('./TablePageRow')) + +const TablePageRow = (props: JSX.IntrinsicAttributes & TablePageRowProps): JSX.Element => ( + }> + + +) + +export default TablePageRow diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.settings.ts b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.settings.ts new file mode 100644 index 00000000..e91f968d --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.settings.ts @@ -0,0 +1,17 @@ +export const TablePageRowFieldsClasses: Record = { + 0: 'data-fields-default', + 1: 'data-fields-1', + 2: 'data-fields-2', + 3: 'data-fields-3', + 4: 'data-fields-4', + 5: 'data-fields-5', + 6: 'data-fields-6', + 7: 'data-fields-7', + 8: 'data-fields-8', + 9: 'data-fields-9', + 10: 'data-fields-10', + 11: 'data-fields-11', + 12: 'data-fields-12' +} + +export const TablePageRowDefaultFieldsValue = 0 diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.stories.tsx b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.stories.tsx new file mode 100644 index 00000000..b081d6b0 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import TableDataCell from '@/components/02-molecules/TableDataCell/TableDataCell' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import TablePageRow from './TablePageRow' + +type StoryType = StoryObj + +const meta: Meta = { + component: TablePageRow, + title: '01-Atoms/TablePageRow', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + children: [ + + Cell1 + , + + Cell2 + + ] + } +} diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.tsx b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.tsx new file mode 100644 index 00000000..ec731ea0 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { TablePageRowProps } from './TablePageRow.types' + +import { TablePageRowFieldsClasses } from './TablePageRow.settings' + +const TablePageRow: FC = ({ children }) => { + const fields = Array.isArray(children) ? children.length : 0 + + const cssClass = TablePageRowFieldsClasses[fields] ?? 'data-fields-default' + + return ( + + {children} + + ) +} + +export default TablePageRow diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.types.ts b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.types.ts new file mode 100644 index 00000000..d2030221 --- /dev/null +++ b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface TablePageRowProps { + children: ReactNode +} diff --git a/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.utils.ts b/packages/frontend-react/src/components/01-atoms/TablePageRow/TablePageRow.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.lazy.tsx b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.lazy.tsx new file mode 100644 index 00000000..aaa39f12 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { AccountStatusBadgeProps } from './AccountStatusBadge.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyAccountStatusBadge = lazy(async () => await import('./AccountStatusBadge')) + +const AccountStatusBadge = (props: JSX.IntrinsicAttributes & AccountStatusBadgeProps): JSX.Element => ( + }> + + +) + +export default AccountStatusBadge diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.module.css b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.module.css new file mode 100644 index 00000000..3cdb5f84 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.module.css @@ -0,0 +1,11 @@ +.Icon { + @apply inline h-5 w-5; +} + +.Icon > svg { + @apply inline h-5 w-5 rounded-full p-0; +} + +.Icon > svg > * { + @apply bg-white; +} diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.stories.tsx b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.stories.tsx new file mode 100644 index 00000000..a98de06d --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import Col from '@/components/01-atoms/Col/Col' +import Row from '@/components/01-atoms/Row/Row' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import AccountStatusBadge from './AccountStatusBadge' + +type StoryType = StoryObj + +const meta: Meta = { + component: AccountStatusBadge, + title: '02-Molecules/AccountStatusBadge', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + render: () => ( + <> + + + Active + + + + + Banned + + + + + Inactive + + + + + Pending + + + + + Unknown + + + + ) +} + +export const Active: StoryType = { + args: { + status: 'Active' + } +} + +export const Banned: StoryType = { + args: { + status: 'Banned' + } +} + +export const Inactive: StoryType = { + args: { + status: 'Inactive' + } +} + +export const Pending: StoryType = { + args: { + status: 'Pending' + } +} + +export const Unknown: StoryType = { + args: { + status: 'Unknown' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.tsx b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.tsx new file mode 100644 index 00000000..badb5f14 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.tsx @@ -0,0 +1,45 @@ +import type { FC, JSX } from 'react' + +import { + CheckCircleIcon, + ClockIcon, + NoSymbolIcon, + PauseCircleIcon, + QuestionMarkCircleIcon +} from '@heroicons/react/16/solid' + +import type { AccountStatusBadgeProps } from './AccountStatusBadge.types' + +import styles from './AccountStatusBadge.module.css' + +const BannedIcon = +const PendingIcon = +const ActiveIcon = +const InactiveIcon = +const UnknownIcon = + +const getStatusIcon = (status: string | null): JSX.Element => { + switch (status) { + case 'Active': + return ActiveIcon + case 'Banned': + return BannedIcon + case 'Inactive': + return InactiveIcon + case 'Pending': + return PendingIcon + default: + return UnknownIcon + } +} + +const AccountStatusBadge: FC = ({ status, className }) => { + return ( +
+
{getStatusIcon(status)}
+
 {status}
+
+ ) +} + +export default AccountStatusBadge diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.types.ts b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.types.ts new file mode 100644 index 00000000..102f49f6 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.types.ts @@ -0,0 +1,5 @@ +import type { CastReactElement } from '@/types/utils' + +export interface AccountStatusBadgeProps extends CastReactElement<'div'> { + status: string | null +} diff --git a/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.utils.ts b/packages/frontend-react/src/components/02-molecules/AccountStatusBadge/AccountStatusBadge.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.lazy.tsx new file mode 100644 index 00000000..0d6f0239 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { AdminGuardProps } from './AdminGuard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyAdminGuard = lazy(async () => await import('./AdminGuard')) + +const AdminGuard = (props: JSX.IntrinsicAttributes & AdminGuardProps): JSX.Element => ( + }> + + +) + +export default AdminGuard diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.stories.tsx b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.stories.tsx new file mode 100644 index 00000000..39206927 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import AdminGuard from './AdminGuard' + +type StoryType = StoryObj + +const meta: Meta = { + component: AdminGuard, + title: '02-Molecules/AdminGuard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'AdminGuard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.tsx b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.tsx new file mode 100644 index 00000000..41fb72d7 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react' + +import { Navigate, useLocation } from '@tanstack/react-router' + +import type { AdminGuardProps } from './AdminGuard.types' + +import useAuth from '@/lib/hooks/useAuth' + +const AdminGuard: FC = ({ children }) => { + const { isAuthenticated, currentUser } = useAuth() + + const pathname = useLocation({ + select: (location) => location.pathname + }) + + if (!isAuthenticated) return + + if (!(currentUser?.hasPermission('administrator') ?? false)) return + + return <>{children} +} + +export default AdminGuard diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.types.ts b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.types.ts new file mode 100644 index 00000000..171f45cc --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface AdminGuardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.utils.ts b/packages/frontend-react/src/components/02-molecules/AdminGuard/AdminGuard.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.lazy.tsx b/packages/frontend-react/src/components/02-molecules/Button/Button.lazy.tsx new file mode 100644 index 00000000..51a108f6 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ButtonProps } from './Button.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyButton = lazy(async () => await import('./Button')) + +const Button = (props: JSX.IntrinsicAttributes & ButtonProps): JSX.Element => ( + }> + + +) + +export default Button diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.stories.tsx b/packages/frontend-react/src/components/02-molecules/Button/Button.stories.tsx new file mode 100644 index 00000000..0bf7fd8b --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.stories.tsx @@ -0,0 +1,86 @@ +import type { ButtonProps } from './Button.types' +import type { Meta, StoryObj } from '@storybook/react' + +import Col from '@/components/01-atoms/Col/Col' +import Row from '@/components/01-atoms/Row/Row' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook/common' + +import Button from './Button' + +type StoryType = StoryObj + +const meta: Meta = { + title: '02-Molecules/Button', + component: Button, + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + render: () => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export const Primary: StoryType = { args: { variant: 'primary', children: 'Primary' } } +export const Secondary: StoryType = { args: { variant: 'secondary', children: 'Secondary' } } +export const Success: StoryType = { args: { variant: 'success', children: 'Success' } } +export const Warning: StoryType = { args: { variant: 'warning', children: 'Warning' } } +export const Danger: StoryType = { args: { variant: 'danger', children: 'Danger' } } +export const Info: StoryType = { args: { variant: 'info', children: 'Info' } } +export const Light: StoryType = { args: { variant: 'light', children: 'Light' } } +export const Dark: StoryType = { args: { variant: 'dark', children: 'Dark' } } +export const Link: StoryType = { args: { variant: 'link', children: 'Link' } } diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.styles.ts b/packages/frontend-react/src/components/02-molecules/Button/Button.styles.ts new file mode 100644 index 00000000..a3ac2b31 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.styles.ts @@ -0,0 +1,18 @@ +import type { ButtonVariantTypes } from './Button.types' + +const variants: Record = { + primary: 'btn-primary', + secondary: 'btn-secondary', + success: 'btn-success', + warning: 'btn-warning', + danger: 'btn-danger', + info: 'btn-info', + light: 'btn-light', + dark: 'btn-dark', + link: 'btn-link', + none: 'btn-none' +} + +const styles = { variants } + +export default styles diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.tsx b/packages/frontend-react/src/components/02-molecules/Button/Button.tsx new file mode 100644 index 00000000..c254d6ce --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.tsx @@ -0,0 +1,31 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { ButtonProps } from './Button.types' + +import styles from './Button.styles' + +const Button: FC = ({ children, circle, className, small, variant, ...restProps }) => { + variant ??= 'primary' + small ??= false + circle ??= false + + return ( + + ) +} + +export default Button diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.types.ts b/packages/frontend-react/src/components/02-molecules/Button/Button.types.ts new file mode 100644 index 00000000..3b875109 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Button/Button.types.ts @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react' + +import type { CastReactElement } from '@/types/utils' + +export type ButtonVariantTypes = + | 'primary' + | 'secondary' + | 'success' + | 'warning' + | 'danger' + | 'info' + | 'light' + | 'dark' + | 'link' + | 'none' + +export interface ButtonProps extends CastReactElement<'button'> { + children?: ReactNode + circle?: boolean + small?: boolean + variant?: ButtonVariantTypes +} diff --git a/packages/frontend-react/src/components/02-molecules/Button/Button.utils.ts b/packages/frontend-react/src/components/02-molecules/Button/Button.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.lazy.tsx b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.lazy.tsx new file mode 100644 index 00000000..8c156dac --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ComplexChartProps } from './ComplexChart.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyComplexChart = lazy(async () => await import('./ComplexChart')) + +const ComplexChart = (props: JSX.IntrinsicAttributes & ComplexChartProps): JSX.Element => ( + }> + + +) + +export default ComplexChart diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.stories.tsx b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.stories.tsx new file mode 100644 index 00000000..cd91317c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.stories.tsx @@ -0,0 +1,34 @@ +import type { JSX } from 'react' + +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import ComplexChart from './ComplexChart' + +type StoryType = StoryObj + +const meta: Meta = { + component: ComplexChart, + title: '02-Molecules/ComplexChart', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + component: (): JSX.Element => <>Test, + displayName: 'Test', + height: 350, + width: 350 + } +} diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.tsx b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.tsx new file mode 100644 index 00000000..6035c656 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.tsx @@ -0,0 +1,24 @@ +import type { FC } from 'react' + +import type { ComplexChartProps } from './ComplexChart.types' + +type DimensionProps = + | Record + | { width: number } + | { height: number } + | { width: number; height: number } + +const ComplexChart: FC = ({ width, height, component: Component, ...chartProps }) => { + const dimensionProps: DimensionProps = {} + + if (width != null) dimensionProps.width = width + if (height != null) dimensionProps.height = height + + return ( +
+ +
+ ) +} + +export default ComplexChart diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.types.ts b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.types.ts new file mode 100644 index 00000000..1c76c34c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.types.ts @@ -0,0 +1,7 @@ +import type { ElementType, ComponentType } from 'react' + +export type ComplexChartProps = Omit & { + height?: number + width?: number + component: ElementType +} diff --git a/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.utils.ts b/packages/frontend-react/src/components/02-molecules/ComplexChart/ComplexChart.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.lazy.tsx b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.lazy.tsx new file mode 100644 index 00000000..caeea808 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { DoughnutChartProps } from './DoughnutChart.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyDoughnutChart = lazy(async () => await import('./DoughnutChart')) + +const DoughnutChart = (props: JSX.IntrinsicAttributes & DoughnutChartProps): JSX.Element => ( + }> + + +) + +export default DoughnutChart diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.stories.tsx b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.stories.tsx new file mode 100644 index 00000000..ce15ec8e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.stories.tsx @@ -0,0 +1,23 @@ +import type { JSX } from 'react' + +import type { ChartProps } from 'react-chartjs-2' + +import DoughnutChart from './DoughnutChart' + +export default { + title: '02-Molecules/DoughnutChart' +} + +const storyData: ChartProps<'doughnut'>['data'] = { + labels: ['Red', 'Blue', 'Yellow'], + datasets: [ + { + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: ['rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)'], + hoverOffset: 4 + } + ] +} + +export const Default = (): JSX.Element => diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.tsx b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.tsx new file mode 100644 index 00000000..dddf0ac5 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.tsx @@ -0,0 +1,13 @@ +import type { FC } from 'react' + +import { Doughnut } from 'react-chartjs-2' + +import type { DoughnutChartProps } from './DoughnutChart.types' + +const DoughnutChart: FC = ({ width, height, ...doughnutProps }) => ( +
+ +
+) + +export default DoughnutChart diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.types.ts b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.types.ts new file mode 100644 index 00000000..18944e0f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.types.ts @@ -0,0 +1,6 @@ +import type { ChartProps } from 'react-chartjs-2' + +export interface DoughnutChartProps extends ChartProps<'doughnut'> { + height?: number | string + width?: number | string +} diff --git a/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.utils.ts b/packages/frontend-react/src/components/02-molecules/DoughnutChart/DoughnutChart.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.lazy.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.lazy.tsx new file mode 100644 index 00000000..85cc4b50 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { EnabledCheckMarkProps } from './EnabledCheckMark.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyEnabledCheckMark = lazy(async () => await import('./EnabledCheckMark')) + +const EnabledCheckMark = (props: JSX.IntrinsicAttributes & EnabledCheckMarkProps): JSX.Element => ( + }> + + +) + +export default EnabledCheckMark diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.stories.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.stories.tsx new file mode 100644 index 00000000..65e69349 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import EnabledCheckMark from './EnabledCheckMark' + +type StoryType = StoryObj + +const meta: Meta = { + component: EnabledCheckMark, + title: '02-Molecules/EnabledCheckMark', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + checked: true + } +} + +export const Checked: StoryType = { + args: { + checked: true + } +} + +export const Unchecked: StoryType = { + args: { + checked: false + } +} diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.test.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.test.tsx new file mode 100644 index 00000000..c221fbef --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import EnabledCheckMark from './EnabledCheckMark' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.tsx b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.tsx new file mode 100644 index 00000000..695b9bcb --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.tsx @@ -0,0 +1,36 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { EnabledCheckMarkProps } from './EnabledCheckMark.types' + +import FontAwesomeIcon from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon' + +const EnabledCheckMark: FC = ({ + checked, + positiveIcon, + positiveIconColour, + negativeIcon, + negativeHighlight, + negativeHighlightColour +}) => { + checked ??= false + positiveIcon ??= 'check-circle' + positiveIconColour ??= 'text-green-500' + negativeIcon ??= 'check-circle' + negativeHighlight ??= false + negativeHighlightColour ??= 'text-red-500' + + const negativeColour = negativeHighlight ? negativeHighlightColour : 'text-gray-500' + + return ( +
+ +
+ ) +} + +export default EnabledCheckMark diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.types.ts b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.types.ts new file mode 100644 index 00000000..decd3963 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.types.ts @@ -0,0 +1,10 @@ +import type { IconProp } from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types' + +export interface EnabledCheckMarkProps { + checked?: boolean + positiveIcon?: IconProp + positiveIconColour?: string + negativeIcon?: IconProp + negativeHighlight?: boolean + negativeHighlightColour?: string +} diff --git a/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.utils.ts b/packages/frontend-react/src/components/02-molecules/EnabledCheckMark/EnabledCheckMark.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.lazy.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.lazy.tsx new file mode 100644 index 00000000..b589430c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormColProps } from './FormCol.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormCol = lazy(async () => await import('./FormCol')) + +const FormCol = (props: JSX.IntrinsicAttributes & FormColProps): JSX.Element => ( + }> + + +) + +export default FormCol diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.stories.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.stories.tsx new file mode 100644 index 00000000..2a486fe5 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormCol from './FormCol' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormCol, + title: '02-Molecules/FormCol', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormCol' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.test.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.test.tsx new file mode 100644 index 00000000..139d4708 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import FormCol from './FormCol' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.tsx b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.tsx new file mode 100644 index 00000000..06f92186 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.tsx @@ -0,0 +1,21 @@ +import { useMemo, type FC } from 'react' + +import { clsx } from 'clsx' + +import type { FormColProps } from './FormCol.types' + +import Col from '@/components/01-atoms/Col/Col' + +import { coerceErrorBoolean } from '@/lib/utils' + +const FormCol: FC = ({ children, className, error }) => { + const errorValue = useMemo(() => coerceErrorBoolean(error), [error]) + + return ( + + {children} + + ) +} + +export default FormCol diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.types.ts b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.types.ts new file mode 100644 index 00000000..db8034a0 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { ColProps } from '@/components/01-atoms/Col/Col.types' + +export interface FormColProps extends ColProps { + children?: ReactNode + error?: unknown +} diff --git a/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.utils.ts b/packages/frontend-react/src/components/02-molecules/FormCol/FormCol.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.lazy.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.lazy.tsx new file mode 100644 index 00000000..283f3cd3 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormRowProps } from './FormRow.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormRow = lazy(async () => await import('./FormRow')) + +const FormRow = (props: JSX.IntrinsicAttributes & FormRowProps): JSX.Element => ( + }> + + +) + +export default FormRow diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.stories.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.stories.tsx new file mode 100644 index 00000000..6aba012c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormRow from './FormRow' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormRow, + title: '02-Molecules/FormRow', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormRow' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.test.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.test.tsx new file mode 100644 index 00000000..12d38dc3 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import FormRow from './FormRow' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.tsx b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.tsx new file mode 100644 index 00000000..7f4bfc54 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.tsx @@ -0,0 +1,21 @@ +import { useMemo, type FC } from 'react' + +import { clsx } from 'clsx' + +import type { FormRowProps } from './FormRow.types' + +import Row from '@/components/01-atoms/Row/Row' + +import { coerceErrorBoolean } from '@/lib/utils' + +const FormRow: FC = ({ children, className, error }) => { + const errorValue = useMemo(() => coerceErrorBoolean(error), [error]) + + return ( + + {children} + + ) +} + +export default FormRow diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.types.ts b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.types.ts new file mode 100644 index 00000000..58558e47 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' + +import type { RowProps } from '@/components/01-atoms/Row/Row.types' + +export interface FormRowProps extends RowProps { + children?: ReactNode + error?: unknown +} diff --git a/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.utils.ts b/packages/frontend-react/src/components/02-molecules/FormRow/FormRow.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.lazy.tsx b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.lazy.tsx new file mode 100644 index 00000000..eb06f8d9 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { InputGroupProps } from './InputGroup.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyInputGroup = lazy(async () => await import('./InputGroup')) + +const InputGroup = (props: JSX.IntrinsicAttributes & InputGroupProps): JSX.Element => ( + }> + + +) + +export default InputGroup diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.stories.tsx b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.stories.tsx new file mode 100644 index 00000000..ba2bf691 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import InputGroup from './InputGroup' + +type StoryType = StoryObj + +const meta: Meta = { + component: InputGroup, + title: '02-Molecules/InputGroup', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'InputGroup' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.tsx b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.tsx new file mode 100644 index 00000000..5e26925e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.tsx @@ -0,0 +1,22 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { InputGroupProps } from './InputGroup.types' + +import Col from '@/components/01-atoms/Col/Col' +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import Row from '@/components/01-atoms/Row/Row' + +const InputGroup: FC = ({ children, className, prepend }) => ( + + + +
{prepend}
+
+ {children} + +
+) + +export default InputGroup diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.types.ts b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.types.ts new file mode 100644 index 00000000..300ca1da --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface InputGroupProps { + children?: ReactNode + className?: string + prepend?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.utils.ts b/packages/frontend-react/src/components/02-molecules/InputGroup/InputGroup.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.lazy.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.lazy.tsx new file mode 100644 index 00000000..af28aaa3 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { LinkButtonProps } from './LinkButton.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyLinkButton = lazy(async () => await import('./LinkButton')) + +const LinkButton = (props: JSX.IntrinsicAttributes & LinkButtonProps): JSX.Element => ( + }> + + +) + +export default LinkButton diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.stories.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.stories.tsx new file mode 100644 index 00000000..067868d8 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import LinkButton from './LinkButton' + +type StoryType = StoryObj + +const meta: Meta = { + component: LinkButton, + title: '02-Molecules/LinkButton', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'LinkButton' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.test.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.test.tsx new file mode 100644 index 00000000..a7b70186 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import LinkButton from './LinkButton' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render(LinkButton) + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.tsx b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.tsx new file mode 100644 index 00000000..25a7e61e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.tsx @@ -0,0 +1,33 @@ +import type { FC } from 'react' + +import { Link } from '@tanstack/react-router' +import { clsx } from 'clsx' + +import type { LinkButtonProps } from './LinkButton.types' + +import styles from '@/components/02-molecules/Button/Button.styles' + +const LinkButton: FC = ({ children, circle, className, small, variant, to }) => { + circle ??= false + small ??= false + variant ??= 'primary' + + return ( +
+ + {children} + +
+ ) +} + +export default LinkButton diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.types.ts b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.types.ts new file mode 100644 index 00000000..e321bd61 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.types.ts @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +import type { LinkProps } from '@tanstack/react-router' + +import type { ButtonProps } from '@/components/02-molecules/Button/Button.types' + +export interface LinkButtonProps extends ButtonProps, LinkProps { + children: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.utils.ts b/packages/frontend-react/src/components/02-molecules/LinkButton/LinkButton.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.stories.tsx b/packages/frontend-react/src/components/02-molecules/Loading/Loading.stories.tsx new file mode 100644 index 00000000..7f5a5212 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import Loading from './Loading' + +type StoryType = StoryObj + +const meta: Meta = { + component: Loading, + title: '02-Molecules/Loading', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'Loading' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.tsx b/packages/frontend-react/src/components/02-molecules/Loading/Loading.tsx new file mode 100644 index 00000000..fac54eaf --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import FadeLoader from 'react-spinners/FadeLoader' + +import type { LoadingProps } from './Loading.types' + +import { DefaultLoadingProps } from './Loading.utils' + +const Loading: FC = (overrideProps) => { + overrideProps ??= {} + + return ( +
+
+ +
+
+ ) +} + +export default Loading diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.types.ts b/packages/frontend-react/src/components/02-molecules/Loading/Loading.types.ts new file mode 100644 index 00000000..e11b93e8 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.types.ts @@ -0,0 +1,3 @@ +import type { LoaderHeightWidthRadiusProps } from 'react-spinners/helpers/props' + +export interface LoadingProps extends LoaderHeightWidthRadiusProps {} diff --git a/packages/frontend-react/src/components/02-molecules/Loading/Loading.utils.ts b/packages/frontend-react/src/components/02-molecules/Loading/Loading.utils.ts new file mode 100644 index 00000000..c18316b1 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Loading/Loading.utils.ts @@ -0,0 +1,7 @@ +export const DefaultLoadingProps = { + height: 32, + width: 5, + radius: 5, + margin: 10, + color: '#35a435' +} diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.lazy.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.lazy.tsx new file mode 100644 index 00000000..398d9589 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { NotFoundComponentProps } from './NotFoundComponent.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyNotFoundComponent = lazy(async () => await import('./NotFoundComponent')) + +const NotFoundComponent = (props: JSX.IntrinsicAttributes & NotFoundComponentProps): JSX.Element => ( + }> + + +) + +export default NotFoundComponent diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.stories.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.stories.tsx new file mode 100644 index 00000000..cde0d9c6 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import NotFoundComponent from './NotFoundComponent' + +type StoryType = StoryObj + +const meta: Meta = { + component: NotFoundComponent, + title: '02-Molecules/NotFoundComponent', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'NotFoundComponent' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.test.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.test.tsx new file mode 100644 index 00000000..7654e84a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import NotFoundComponent from './NotFoundComponent' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.tsx b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.tsx new file mode 100644 index 00000000..b1f738ff --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.tsx @@ -0,0 +1,14 @@ +import type { FC } from 'react' + +import { Link } from '@tanstack/react-router' + +import type { NotFoundComponentProps } from './NotFoundComponent.types' + +const NotFoundComponent: FC = () => ( +
+

Not found!

+ Go home +
+) + +export default NotFoundComponent diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.types.ts b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.types.ts new file mode 100644 index 00000000..98e9c695 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.types.ts @@ -0,0 +1 @@ +export interface NotFoundComponentProps {} diff --git a/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.utils.ts b/packages/frontend-react/src/components/02-molecules/NotFoundComponent/NotFoundComponent.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.lazy.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.lazy.tsx new file mode 100644 index 00000000..e207a4cd --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { PrivilegeIconProps } from './PrivilegeIcon.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyPrivilegeIcon = lazy(async () => await import('./PrivilegeIcon')) + +const PrivilegeIcon = (props: JSX.IntrinsicAttributes & PrivilegeIconProps): JSX.Element => ( + }> + + +) + +export default PrivilegeIcon diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.stories.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.stories.tsx new file mode 100644 index 00000000..bb8a67d5 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' + +import PrivilegeIcon from './PrivilegeIcon' + +type StoryType = StoryObj + +const meta: Meta = { + component: PrivilegeIcon, + title: '02-Molecules/PrivilegeIcon', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +export const Default: StoryType = { + args: { + icon: 'key' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.test.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.test.tsx new file mode 100644 index 00000000..8206069d --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import PrivilegeIcon from './PrivilegeIcon' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.tsx new file mode 100644 index 00000000..637a246a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.tsx @@ -0,0 +1,26 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { PrivilegeIconProps } from './PrivilegeIcon.types' + +import FontAwesomeIcon from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon' + +import type { IconName } from '@/types/fontawesome' + +import { getPrivilegeIconSettings } from './PrivilegeIcon.ui' + +const PrivilegeIcon: FC = ({ icon, className, size }) => { + const settings = getPrivilegeIconSettings(icon) + + return ( + + ) +} + +export default PrivilegeIcon diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.types.ts b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.types.ts new file mode 100644 index 00000000..63d11099 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.types.ts @@ -0,0 +1,11 @@ +import type { FontAwesomeIconProps } from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon.types' + +export interface PrivilegeIconProps extends Omit { + icon: string | null | undefined + className?: string +} + +export interface PrivilegeIconSettings { + icon: string | null | undefined + css: string | null | undefined +} diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.ui.tsx b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.ui.tsx new file mode 100644 index 00000000..d19f40f5 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.ui.tsx @@ -0,0 +1,17 @@ +import type { PrivilegeIconSettings } from './PrivilegeIcon.types' + +import { isString } from '@/lib/guards/common' +import { checkValidIcon } from '@/lib/ui/fontawesome' + +export const getPrivilegeIconSettings = (icon: string | null | undefined): PrivilegeIconSettings => { + if (isString(icon) && icon !== '' && !checkValidIcon(icon)) { + return { icon: 'link-slash', css: 'opacity-50' } + } else if (!checkValidIcon(icon)) { + return { + icon: 'notdef', + css: 'opacity-25' + } + } else { + return { icon, css: null } + } +} diff --git a/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.utils.ts b/packages/frontend-react/src/components/02-molecules/PrivilegeIcon/PrivilegeIcon.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.lazy.tsx new file mode 100644 index 00000000..b4e8f513 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectAdminDashboardProps } from './RedirectAdminDashboard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectAdminDashboard = lazy(async () => await import('./RedirectAdminDashboard')) + +const RedirectAdminDashboard = (props: JSX.IntrinsicAttributes & RedirectAdminDashboardProps): JSX.Element => ( + }> + + +) + +export default RedirectAdminDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.stories.tsx new file mode 100644 index 00000000..6bcfe03f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectAdminDashboard from './RedirectAdminDashboard' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectAdminDashboard, + title: '02-Molecules/RedirectAdminDashboard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectAdminDashboard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.tsx b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.tsx new file mode 100644 index 00000000..5f43528c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectAdminDashboardProps } from './RedirectAdminDashboard.types' + +const RedirectAdminDashboard: FC = () => + +export default RedirectAdminDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.types.ts new file mode 100644 index 00000000..d77fcb8c --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectAdminDashboardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectAdminDashboard/RedirectAdminDashboard.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.lazy.tsx new file mode 100644 index 00000000..3f3b6bdf --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectLoginProps } from './RedirectLogin.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectLogin = lazy(async () => await import('./RedirectLogin')) + +const RedirectLogin = (props: JSX.IntrinsicAttributes & RedirectLoginProps): JSX.Element => ( + }> + + +) + +export default RedirectLogin diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.stories.tsx new file mode 100644 index 00000000..a4cfa255 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectLogin from './RedirectLogin' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectLogin, + title: '02-Molecules/RedirectLogin', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectLogin' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.tsx b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.tsx new file mode 100644 index 00000000..3330cf3a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectLoginProps } from './RedirectLogin.types' + +const RedirectLogin: FC = () => + +export default RedirectLogin diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.types.ts new file mode 100644 index 00000000..75541014 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectLoginProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectLogin/RedirectLogin.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.lazy.tsx new file mode 100644 index 00000000..766c56bd --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectRootProps } from './RedirectRoot.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectRoot = lazy(async () => await import('./RedirectRoot')) + +const RedirectRoot = (props: JSX.IntrinsicAttributes & RedirectRootProps): JSX.Element => ( + }> + + +) + +export default RedirectRoot diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.stories.tsx new file mode 100644 index 00000000..46360b6f --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectRoot from './RedirectRoot' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectRoot, + title: '02-Molecules/RedirectRoot', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectRoot' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.tsx b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.tsx new file mode 100644 index 00000000..0be87efe --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectRootProps } from './RedirectRoot.types' + +const RedirectRoot: FC = () => + +export default RedirectRoot diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.types.ts new file mode 100644 index 00000000..6d7a2d3e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectRootProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectRoot/RedirectRoot.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.lazy.tsx new file mode 100644 index 00000000..342dd171 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { RedirectUserDashboardProps } from './RedirectUserDashboard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyRedirectUserDashboard = lazy(async () => await import('./RedirectUserDashboard')) + +const RedirectUserDashboard = (props: JSX.IntrinsicAttributes & RedirectUserDashboardProps): JSX.Element => ( + }> + + +) + +export default RedirectUserDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.stories.tsx b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.stories.tsx new file mode 100644 index 00000000..8bf9b407 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import RedirectUserDashboard from './RedirectUserDashboard' + +type StoryType = StoryObj + +const meta: Meta = { + component: RedirectUserDashboard, + title: '02-Molecules/RedirectUserDashboard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'RedirectUserDashboard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.tsx b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.tsx new file mode 100644 index 00000000..9518e2a7 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.tsx @@ -0,0 +1,9 @@ +import type { FC } from 'react' + +import { Navigate } from '@tanstack/react-router' + +import type { RedirectUserDashboardProps } from './RedirectUserDashboard.types' + +const RedirectUserDashboard: FC = () => + +export default RedirectUserDashboard diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.types.ts b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.types.ts new file mode 100644 index 00000000..8872e1f5 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface RedirectUserDashboardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.utils.ts b/packages/frontend-react/src/components/02-molecules/RedirectUserDashboard/RedirectUserDashboard.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.lazy.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.lazy.tsx new file mode 100644 index 00000000..b47de99d --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { SpaciousRowProps } from './SpaciousRow.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazySpaciousRow = lazy(async () => await import('./SpaciousRow')) + +const SpaciousRow = (props: JSX.IntrinsicAttributes & SpaciousRowProps): JSX.Element => ( + }> + + +) + +export default SpaciousRow diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.stories.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.stories.tsx new file mode 100644 index 00000000..9fae0597 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import SpaciousRow from './SpaciousRow' + +type StoryType = StoryObj + +const meta: Meta = { + component: SpaciousRow, + title: '02-Molecules/SpaciousRow', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'SpaciousRow' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.test.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.test.tsx new file mode 100644 index 00000000..06997662 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import SpaciousRow from './SpaciousRow' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.tsx b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.tsx new file mode 100644 index 00000000..c105f762 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.tsx @@ -0,0 +1,15 @@ +import type { FC } from 'react' + +import { clsx } from 'clsx' + +import type { SpaciousRowProps } from './SpaciousRow.types' + +import Row from '@/components/01-atoms/Row/Row' + +const SpaciousRow: FC = ({ className, children, ...restProps }) => ( + + {children} + +) + +export default SpaciousRow diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.types.ts b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.types.ts new file mode 100644 index 00000000..6a8cb5f4 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +import type { RowProps } from '@/components/01-atoms/Row/Row.types' + +export interface SpaciousRowProps extends RowProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.utils.ts b/packages/frontend-react/src/components/02-molecules/SpaciousRow/SpaciousRow.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.lazy.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.lazy.tsx new file mode 100644 index 00000000..48891aed --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { TableDataCellProps } from './TableDataCell.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyTableDataCell = lazy(async () => await import('./TableDataCell')) + +const TableDataCell = (props: JSX.IntrinsicAttributes & TableDataCellProps): JSX.Element => ( + }> + + +) + +export default TableDataCell diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.stories.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.stories.tsx new file mode 100644 index 00000000..852a4dc2 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import TableDataCell from './TableDataCell' + +type StoryType = StoryObj + +const meta: Meta = { + component: TableDataCell, + title: '02-Molecules/TableDataCell', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'TableDataCell' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.test.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.test.tsx new file mode 100644 index 00000000..67b27e31 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import TableDataCell from './TableDataCell' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.tsx b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.tsx new file mode 100644 index 00000000..25b68d0e --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.tsx @@ -0,0 +1,26 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { TableDataCellProps } from './TableDataCell.types' + +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import Popover from '@/components/01-atoms/Popover/Popover' + +const TableDataCell: FC = ({ className, children, condition }) => { + condition ??= true + + return ( + + + {(typeof children === 'string' && children.length < 16) || typeof children !== 'string' ? ( + children + ) : ( + + )} + + + ) +} + +export default TableDataCell diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.types.ts b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.types.ts new file mode 100644 index 00000000..3ca4a7e3 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.types.ts @@ -0,0 +1,5 @@ +import type { ConditionalProps } from '@/components/01-atoms/Conditional/Conditional.types' + +import type { CastReactElement } from '@/types/utils' + +export type TableDataCellProps = CastReactElement<'td'> & Partial diff --git a/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.utils.ts b/packages/frontend-react/src/components/02-molecules/TableDataCell/TableDataCell.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.lazy.tsx b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.lazy.tsx new file mode 100644 index 00000000..164f0ea7 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { ToggleProps } from './Toggle.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyToggle = lazy(async () => await import('./Toggle')) + +const Toggle = (props: JSX.IntrinsicAttributes & ToggleProps): JSX.Element => ( + }> + + +) + +export default Toggle diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.stories.tsx b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.stories.tsx new file mode 100644 index 00000000..1c06a8e1 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import Toggle from './Toggle' + +type StoryType = StoryObj + +const meta: Meta = { + component: Toggle, + title: '02-Molecules/Toggle', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + render: () => ( + <> + Unchecked + Checked + Default and Disabled + + Unchecked and Disabled + + + Checked and Disabled + + + ) +} + +export const Unchecked = {} + +export const Checked = { + args: { checked: true } +} + +export const Disabled = { + args: { disabled: true } +} diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.tsx b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.tsx new file mode 100644 index 00000000..dfe9e456 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState, type FC } from 'react' + +import { CheckCircleIcon } from '@heroicons/react/24/solid' +import clsx from 'clsx' + +import type { ToggleProps } from './Toggle.types' + +const Toggle: FC = ({ children, checked, disabled, onChange, ...restProps }) => { + disabled ??= false + + const [isChecked, setChecked] = useState(checked ?? false) + + const toggleChecked = (): void => { + if (!disabled) + if (onChange != null && checked != null) onChange(!checked) + else setChecked(!isChecked) + } + + useEffect(() => { + if (onChange != null && !disabled && checked == null) onChange(isChecked) + }, [checked, disabled, isChecked, onChange]) + + useEffect(() => { + if (checked != null) setChecked(checked) + }, [checked]) + + return ( + + ) +} + +export default Toggle diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.types.ts b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.types.ts new file mode 100644 index 00000000..96b9e7ce --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.types.ts @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +export interface ToggleProps { + children?: ReactNode + checked?: boolean + disabled?: boolean + onChange?: (val: boolean) => void + id?: string +} diff --git a/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.utils.ts b/packages/frontend-react/src/components/02-molecules/Toggle/Toggle.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.lazy.tsx b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.lazy.tsx new file mode 100644 index 00000000..08dd9422 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { UnderConstructionBannerProps } from './UnderConstructionBanner.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyUnderConstructionBanner = lazy(async () => await import('./UnderConstructionBanner')) + +const UnderConstructionBanner = (props: JSX.IntrinsicAttributes & UnderConstructionBannerProps): JSX.Element => ( + }> + + +) + +export default UnderConstructionBanner diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.stories.tsx b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.stories.tsx new file mode 100644 index 00000000..18afa417 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import UnderConstructionBanner from './UnderConstructionBanner' + +type StoryType = StoryObj + +const meta: Meta = { + component: UnderConstructionBanner, + title: '02-Molecules/UnderConstructionBanner', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'UnderConstructionBanner' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.tsx b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.tsx new file mode 100644 index 00000000..f4e281da --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.tsx @@ -0,0 +1,31 @@ +import type { FC } from 'react' + +import type { UnderConstructionBannerProps } from './UnderConstructionBanner.types' + +import Col from '@/components/01-atoms/Col/Col' +import Row from '@/components/01-atoms/Row/Row' + +const UnderConstructionBanner: FC = () => ( +
+ + + +
+
+ 90s-style animated construction GIF/JIF +
+
+

Under Construction

+
+
+ + +
+
+) + +export default UnderConstructionBanner diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.types.ts b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.types.ts new file mode 100644 index 00000000..dd0aaefc --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface UnderConstructionBannerProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.utils.ts b/packages/frontend-react/src/components/02-molecules/UnderConstructionBanner/UnderConstructionBanner.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.lazy.tsx new file mode 100644 index 00000000..2d08ddb5 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { UserGuardProps } from './UserGuard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyUserGuard = lazy(async () => await import('./UserGuard')) + +const UserGuard = (props: JSX.IntrinsicAttributes & UserGuardProps): JSX.Element => ( + }> + + +) + +export default UserGuard diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.stories.tsx b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.stories.tsx new file mode 100644 index 00000000..0b2787e0 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import UserGuard from './UserGuard' + +type StoryType = StoryObj + +const meta: Meta = { + component: UserGuard, + title: '02-Molecules/UserGuard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'UserGuard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.tsx b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.tsx new file mode 100644 index 00000000..66e20052 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react' + +import { Navigate, useLocation } from '@tanstack/react-router' + +import type { UserGuardProps } from './UserGuard.types' + +import useAuth from '@/lib/hooks/useAuth' + +const UserGuard: FC = ({ children }) => { + const { isAuthenticated } = useAuth() + + const pathname = useLocation({ + select: (location) => location.pathname + }) + + if (!isAuthenticated) { + return + } + + return <>{children} +} + +export default UserGuard diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.types.ts b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.types.ts new file mode 100644 index 00000000..7687ef22 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface UserGuardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.utils.ts b/packages/frontend-react/src/components/02-molecules/UserGuard/UserGuard.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.lazy.tsx b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.lazy.tsx new file mode 100644 index 00000000..f3238e5a --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { UserProfileCardProps } from './UserProfileCard.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyUserProfileCard = lazy(async () => await import('./UserProfileCard')) + +const UserProfileCard = (props: JSX.IntrinsicAttributes & UserProfileCardProps): JSX.Element => ( + }> + + +) + +export default UserProfileCard diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.stories.tsx b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.stories.tsx new file mode 100644 index 00000000..dadd3784 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import UserProfileCard from './UserProfileCard' + +type StoryType = StoryObj + +const meta: Meta = { + component: UserProfileCard, + title: '02-Molecules/UserProfileCard', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'UserProfileCard' + } +} diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.tsx b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.tsx new file mode 100644 index 00000000..9deeda34 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.tsx @@ -0,0 +1,38 @@ +import type { FC } from 'react' + +import { Link } from '@tanstack/react-router' +import MD5 from 'crypto-js/md5' + +import type { UserProfileCardProps } from './UserProfileCard.types' + +import useAuth from '@/lib/hooks/useAuth' + +const UserProfileCard: FC = () => { + const { isAuthenticated, currentUser } = useAuth() + + if (!isAuthenticated || currentUser == null) { + return Loading... + } else { + const userEmail = currentUser.email + + const emailHash: string = MD5(userEmail).toString() + + const gravatarUrl = `https://www.gravatar.com/avatar/${emailHash}?s=64&d=identicon` + + return ( +
+
+ gravatar +
+
+
+ @{currentUser.username} +
+
{currentUser.membership?.title}
+
+
+ ) + } +} + +export default UserProfileCard diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.types.ts b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.types.ts new file mode 100644 index 00000000..76796888 --- /dev/null +++ b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface UserProfileCardProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.utils.ts b/packages/frontend-react/src/components/02-molecules/UserProfileCard/UserProfileCard.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/Card/Card.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/Card.stories.tsx new file mode 100644 index 00000000..f0e1d272 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/Card.stories.tsx @@ -0,0 +1,15 @@ +import type { JSX } from 'react' + +import Card from './Card' + +export default { + title: '03-Particles/Card' +} + +export const Default = (): JSX.Element => ( + + Title + Body + Footer + +) diff --git a/packages/frontend-react/src/components/03-particles/Card/Card.tsx b/packages/frontend-react/src/components/03-particles/Card/Card.tsx new file mode 100644 index 00000000..4ae8aff9 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/Card.tsx @@ -0,0 +1,18 @@ +import type { CardComponent } from './Card.types' + +import Body from './CardBody/CardBody' +import Container from './CardContainer/CardContainer' +import Footer from './CardFooter/CardFooter' +import Header from './CardHeader/CardHeader' + +const Card = structuredClone(Container) +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +Card.prototype.Body = Body +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +Card.prototype.Footer = Footer +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +Card.prototype.Header = Header + +export default Card as CardComponent + +export { Body, Container, Footer, Header } diff --git a/packages/frontend-react/src/components/03-particles/Card/Card.types.ts b/packages/frontend-react/src/components/03-particles/Card/Card.types.ts new file mode 100644 index 00000000..c368628e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/Card.types.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { FC } from 'react' + +import type { CardBodyProps } from './CardBody/CardBody.types' +import type { CardContainerProps } from './CardContainer/CardContainer.types' +import type { CardFooterProps } from './CardFooter/CardFooter.types' +import type { CardHeaderProps } from './CardHeader/CardHeader.types' + +export type CardComponent = FC & { + Body: FC + Header: FC + Footer: FC +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.lazy.tsx new file mode 100644 index 00000000..f44aa8dc --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardBodyProps } from './CardBody.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardBody = lazy(async () => await import('./CardBody')) + +const CardBody = (props: JSX.IntrinsicAttributes & CardBodyProps): JSX.Element => ( + }> + + +) + +export default CardBody diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.stories.tsx new file mode 100644 index 00000000..74f28e55 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardBody from './CardBody' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardBody, + title: '03-Particles/Card/CardBody', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardBody' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.tsx b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.tsx new file mode 100644 index 00000000..c5070e15 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.tsx @@ -0,0 +1,13 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardBodyProps } from './CardBody.types' + +const CardBody: FC = ({ children, className }) => ( +
+ {children} +
+) + +export default CardBody diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.types.ts new file mode 100644 index 00000000..609ec56e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface CardBodyProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardBody/CardBody.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.lazy.tsx new file mode 100644 index 00000000..b9ded66e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardContainerProps } from './CardContainer.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardContainer = lazy(async () => await import('./CardContainer')) + +const CardContainer = (props: JSX.IntrinsicAttributes & CardContainerProps): JSX.Element => ( + }> + + +) + +export default CardContainer diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.stories.tsx new file mode 100644 index 00000000..3529af2a --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardContainer from './CardContainer' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardContainer, + title: '03-Particles/Card/CardContainer', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardContainer' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.tsx b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.tsx new file mode 100644 index 00000000..ae03d283 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardContainerProps } from './CardContainer.types' + +const CardContainer: FC = ({ children, className, error, ...restProps }) => { + error ??= false + + return ( +
+ {children} +
+ ) +} + +export default CardContainer diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.types.ts new file mode 100644 index 00000000..82166e93 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.types.ts @@ -0,0 +1,10 @@ +import type { MouseEventHandler, ReactNode } from 'react' + +export interface CardContainerProps { + children?: ReactNode + className?: string + + error?: boolean + + onMouseLeave?: MouseEventHandler +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardContainer/CardContainer.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.lazy.tsx new file mode 100644 index 00000000..2362cc41 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardFooterProps } from './CardFooter.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardFooter = lazy(async () => await import('./CardFooter')) + +const CardFooter = (props: JSX.IntrinsicAttributes & CardFooterProps): JSX.Element => ( + }> + + +) + +export default CardFooter diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.stories.tsx new file mode 100644 index 00000000..d7aec47e --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardFooter from './CardFooter' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardFooter, + title: '03-Particles/Card/CardFooter', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardFooter' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.tsx b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.tsx new file mode 100644 index 00000000..c06dbe2c --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.tsx @@ -0,0 +1,25 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardFooterProps } from './CardFooter.types' + +const CardFooter: FC = ({ children, className, noGrid }) => { + noGrid ??= false + + return ( +
/^justify-/.test(c)) === false ? 'justify-around' : null + ])} + data-testid='CardFooter' + > + {children} +
+ ) +} + +export default CardFooter diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.types.ts new file mode 100644 index 00000000..8f49f7ca --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.types.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react' + +export interface CardFooterProps { + children?: ReactNode + className?: string + noGrid?: boolean +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardFooter/CardFooter.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.lazy.tsx b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.lazy.tsx new file mode 100644 index 00000000..8e4ba40d --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CardHeaderProps } from './CardHeader.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCardHeader = lazy(async () => await import('./CardHeader')) + +const CardHeader = (props: JSX.IntrinsicAttributes & CardHeaderProps): JSX.Element => ( + }> + + +) + +export default CardHeader diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.stories.tsx b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.stories.tsx new file mode 100644 index 00000000..b10eeb7f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +// import { mockHandlers } from '@/lib/mocking/handlers' + +import CardHeader from './CardHeader' + +type StoryType = StoryObj + +const meta: Meta = { + component: CardHeader, + title: '03-Particles/Card/CardHeader', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CardHeader' + } +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.tsx b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.tsx new file mode 100644 index 00000000..9e3eb46f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.tsx @@ -0,0 +1,13 @@ +import type { FC } from 'react' + +import clsx from 'clsx' + +import type { CardHeaderProps } from './CardHeader.types' + +const CardHeader: FC = ({ children, className }) => ( +
+ {children} +
+) + +export default CardHeader diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.types.ts b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.types.ts new file mode 100644 index 00000000..27dab23f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.types.ts @@ -0,0 +1,6 @@ +import type { ReactNode } from 'react' + +export interface CardHeaderProps { + children?: ReactNode + className?: string +} diff --git a/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.utils.ts b/packages/frontend-react/src/components/03-particles/Card/CardHeader/CardHeader.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.lazy.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.lazy.tsx new file mode 100644 index 00000000..b407d3b4 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { CreateUserModalProps } from './CreateUserModal.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyCreateUserModal = lazy(async () => await import('./CreateUserModal')) + +const CreateUserModal = (props: JSX.IntrinsicAttributes & CreateUserModalProps): JSX.Element => ( + }> + + +) + +export default CreateUserModal diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.stories.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.stories.tsx new file mode 100644 index 00000000..503a194f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import CreateUserModal from './CreateUserModal' + +type StoryType = StoryObj + +const meta: Meta = { + component: CreateUserModal, + title: '03-Particles/CreateUserModal', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'CreateUserModal' + } +} diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.test.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.test.tsx new file mode 100644 index 00000000..c819df86 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.test.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' + +import CreateUserModal from './CreateUserModal' + +it('It should mount', () => { + const container = document.createElement('div') + const root = createRoot(container) + root.render() + root.unmount() +}) diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.tsx b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.tsx new file mode 100644 index 00000000..47419ee1 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.tsx @@ -0,0 +1,15 @@ +import type { FC } from 'react' + +import type { CreateUserModalProps } from './CreateUserModal.types' + +import Button from '@/components/02-molecules/Button/Button' + +const CreateUserModal: FC = () => ( +
+ +
+) + +export default CreateUserModal diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.types.ts b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.types.ts new file mode 100644 index 00000000..4c226d32 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export interface CreateUserModalProps { + children?: ReactNode +} diff --git a/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.utils.ts b/packages/frontend-react/src/components/03-particles/CreateUserModal/CreateUserModal.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.lazy.tsx new file mode 100644 index 00000000..4737b2d7 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlProps } from './FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControl = lazy(async () => await import('./FormControl')) + +const FormControl = (props: JSX.IntrinsicAttributes & FormControlProps): JSX.Element => ( + }> + + +) + +export default FormControl diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.module.css new file mode 100644 index 00000000..134ef3bc --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.module.css @@ -0,0 +1,52 @@ +.Wrapper { + @apply w-full; +} + +.Container { + @apply flex min-h-8 w-full animate-btn flex-row flex-nowrap align-middle leading-6 text-gray-500 outline-none; +} + +.Main:disabled { + @apply bg-slate-100 text-black; +} + +.Main { + @apply w-available animate-btn rounded-lg border-2 border-gray-200 bg-white px-1 py-0.5 align-middle text-gray-800 outline-none; +} + +.PreContent { + @apply h-8 max-w-12 rounded-lg rounded-l rounded-r-none border-y-2 border-l-2 border-r border-gray-200 border-r-slate-200 bg-slate-100 px-2 py-0.5 outline-none; +} + +.PostContent { + @apply h-8 max-w-12 rounded-lg rounded-l-none border-y-2 border-r-2 border-gray-200 border-l-slate-200 bg-slate-100 px-2 py-0.5 text-center outline-none; +} + +.WithPreContent > .Main { + @apply rounded-l-none border-l-0; +} + +.WithPostContent > .Main { + @apply rounded-r-none border-r-0; +} + +.Focus > .Main, +.Focus > .PreContent, +.Focus > .PostContent { + @apply shadow-form-control; +} + +.WithError > .Main, +.WithError > .Main, +.WithError > .PostContent, +.WithError > .PreContent { + @apply shadow-form-error; +} + +.ResetInset { + @apply -ml-6 mt-1.5 h-6 w-6 text-gray-500/50; +} + +.PopoutError { + @apply relative bottom-8 left-16 z-above-and-beyond w-fit text-nowrap rounded-lg border border-slate-500 bg-white/100 p-1 text-center shadow-form-error; +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.stories.tsx new file mode 100644 index 00000000..7b6895ab --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.stories.tsx @@ -0,0 +1,380 @@ +import { useMemo } from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { FormProvider, useForm, type UseFormReturn } from 'react-hook-form' +import { z } from 'zod' + +import type { Meta, StoryObj } from '@storybook/react' + +import Col from '@/components/01-atoms/Col/Col' +import FontAwesomeIcon from '@/components/01-atoms/FontAwesomeIcon/FontAwesomeIcon' +import Row from '@/components/01-atoms/Row/Row' + +import { CenteredContentStorybookDecorator } from '@/lib/ui/storybook' +import { zEmailAddress, zPasswordField, zString, zUserPin } from '@/lib/validators/common' + +import FormControl from './FormControl' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControl, + title: '03-Particles/FormControl', + decorators: [CenteredContentStorybookDecorator] +} + +export default meta + +const FormControlStorySchema = z.object({ + field1: zString, + field2: zString, + field3: zString, + field4: zString, + field5: zPasswordField, + field6: zEmailAddress, + field7: zEmailAddress, + field8: zUserPin, + field9: zUserPin, + field10: zUserPin, + field11: z.union([z.literal('option1'), z.literal('option2'), z.literal('option3'), z.literal('option4')]), + field12: zString.min(3) +}) + +type FormControlStoryForm = z.infer + +const FormControlStoryDefaultValues: FormControlStoryForm = { + field1: '', + field2: '', + field3: '', + field4: '', + field5: 'password', + field6: 'user@example.com', + field7: 'user@example.com', + field8: '1234', + field9: '1234', + field10: '1234', + field11: 'option1', + field12: 'text area' +} + +const useStoryForm = (): UseFormReturn => { + return useForm({ + resolver: zodResolver(FormControlStorySchema), + mode: 'onChange', + defaultValues: FormControlStoryDefaultValues + }) +} + +export const Default: StoryType = { + render: () => { + const form = useStoryForm() + + const errors = useMemo(() => form.formState.errors, [form.formState.errors]) + + return ( + + + +

Unspecified

+ +
+ + + + + + + + +

Disabled

+ +
+ + + + + + + + +

Text

+ +
+ + + + + + + + +

Text with Reset

+ +
+ + + { + // form.setValue('field4', '') + }} + error={errors.field4 != null} + /> + + + + + +

Password

+ +
+ + + + + + + + +

Password

+ +
+ + + } + /> + + + + + +

Password

+ +
+ + + + + + + + +

Password

+ +
+ + + } + infoButton={{ title: 'Password', children: 'Password Field' }} + /> + + + + + +

Email

+ +
+ + + } + placeholder='user@example.com' + error={errors.field6 != null} + /> + + + + + +

Email with Info Button

+ +
+ + + } + placeholder='user@example.com' + infoButton={{ + title: '03-Particles/FormControl', + children: 'Info Content' + }} + error={errors.field7 != null} + /> + + + + + +

Number

+ +
+ + + + + + + + +

Pin

+ +
+ + + + + + + + +

Pin with Info Button

+ +
+ + + + + + + + +

Dropdown

+ +
+ + + + + + + + +

Text Area

+ +
+ + + + + +
+ ) + } +} + +export const Normal: StoryType = { + args: { formKey: 'field3', formType: 'text' } +} + +export const PinWithInfoButton: StoryType = { + render: () => { + const form = useStoryForm() + + return ( + + + + + + + + ) + } +} + +export const PreContent: StoryType = { + args: { + formKey: 'field6', + formType: 'email', + preContent: + } +} + +export const PinInput: StoryType = { + args: { + formKey: 'field8', + formType: 'number', + preContent: '0000' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.tsx new file mode 100644 index 00000000..d04e6244 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.tsx @@ -0,0 +1,46 @@ +import type { FC } from 'react' + +import styles from './FormControl.module.css' +import { + isFormControlDropdownProps, + isFormControlPinProps, + isFormControlTextAreaProps, + type FormControlProps +} from './FormControl.types' +import FormControlDefault from './FormControlDefault/FormControlDefault' +import FormControlDropdown from './FormControlDropdown/FormControlDropdown' +import FormControlPin from './FormControlPin/FormControlPin' +import FormControlTextArea from './FormControlTextArea/FormControlTextArea' + +const FormControl: FC = (props) => { + if (props.formKey == null) + throw new Error('FormControl requires an id to work with react-hook-form: ' + JSON.stringify(props, null, '')) + + if (isFormControlDropdownProps(props)) { + return ( +
+ +
+ ) + } else if (isFormControlTextAreaProps(props)) { + return ( +
+ +
+ ) + } else if (isFormControlPinProps(props)) { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } +} + +export default FormControl diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.types.ts new file mode 100644 index 00000000..cc584c49 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.types.ts @@ -0,0 +1,137 @@ +import type { ReactNode } from 'react' + +import { z } from 'zod' + +import type { InfoButtonProps } from '@/components/05-materials/InfoButton/InfoButton.types' + +import type { CastReactElement } from '@/types/utils' + +export const zFormControlDropdownOption = z.object({ name: z.string(), value: z.string() }) +export const zFormControlDropdownOptions = zFormControlDropdownOption.array() + +export const zFormTypeDefaults = z.union([ + z.literal('button'), + z.literal('checkbox'), + z.literal('color'), + z.literal('date'), + z.literal('datetime-local'), + z.literal('email'), + z.literal('file'), + z.literal('hidden'), + z.literal('image'), + z.literal('month'), + z.literal('number'), + z.literal('password'), + z.literal('radio'), + z.literal('range'), + z.literal('reset'), + z.literal('search'), + z.literal('submit'), + z.literal('tel'), + z.literal('text'), + z.literal('time'), + z.literal('url'), + z.literal('week') +]) +export const zFormTypeDropdown = z.literal('dropdown') +export const zFormTypePin = z.literal('pin') +export const zFormTypeTextArea = z.literal('textarea') + +export const zFormControlDefaultProps = z.object({ formType: zFormTypeDefaults }) +export const zFormControlPinProps = z.object({ formType: zFormTypePin }) +export const zFormControlDropdownProps = z.object({ formType: zFormTypeDropdown }) +export const zFormControlTextAreaProps = z.object({ formType: zFormTypeTextArea }) + +export type FormControlDropdownOption = z.infer +export type FormControlDropdownOptions = z.infer +export type FormTypeDefaults = z.infer +export type FormTypeDropdown = z.infer +export type FormTypePin = z.infer +export type FormTypeTextArea = z.infer + +type HTMLInputTypes = + | 'button' + | 'checkbox' + | 'color' + | 'date' + | 'datetime-local' + | 'email' + | 'file' + | 'hidden' + | 'image' + | 'month' + | 'number' + | 'password' + | 'radio' + | 'range' + | 'reset' + | 'search' + | 'submit' + | 'tel' + | 'text' + | 'time' + | 'url' + | 'week' + +type FormControlExcludeProps = 'name' | 'onBlur' | 'onChange' + +interface FormControlBaseProps { + formKey: string + error?: unknown + errorMessage?: string + reset?: () => void + // onChange?: (change: string) => void + // value?: string +} + +type BaseInputElementProps = Partial, FormControlExcludeProps>> & { + options?: never + preContent?: ReactNode + infoButton?: InfoButtonProps +} + +export type FormControlDefaultProps = BaseInputElementProps & + FormControlBaseProps & { + formType?: HTMLInputTypes + } + +export type FormControlPinProps = BaseInputElementProps & + FormControlBaseProps & { + formType: 'pin' + } + +export type FormControlDropdownProps = Partial, FormControlExcludeProps>> & + FormControlBaseProps & { + formType: 'dropdown' + options: string[] | FormControlDropdownOptions + preContent?: never + infoButton?: never + } + +export type FormControlTextAreaProps = Partial, FormControlExcludeProps>> & + FormControlBaseProps & { + formType: 'textarea' + rows?: HTMLTextAreaElement['rows'] + options?: never + preContent?: never + infoButton?: never + } + +export type FormControlProps = + | FormControlTextAreaProps + | FormControlPinProps + | FormControlDropdownProps + | FormControlDefaultProps + +export const isFormControlDefaultProps = (inp: unknown): inp is FormControlDefaultProps => + zFormControlDefaultProps.safeParse(inp).success +export const isFormControlDropdownOption = (inp: unknown): inp is FormControlDropdownOption => + zFormControlDropdownOption.safeParse(inp).success +export const isFormControlDropdownOptions = (inp: unknown): inp is FormControlDropdownOptions => + zFormControlDropdownOptions.safeParse(inp).success +export const isFormControlDropdownProps = (inp: unknown): inp is FormControlDropdownProps => + zFormControlDropdownProps.safeParse(inp).success +export const isFormControlPinProps = (inp: unknown): inp is FormControlPinProps => + zFormControlPinProps.safeParse(inp).success +export const isFormControlTextAreaProps = (inp: unknown): inp is FormControlTextAreaProps => + zFormControlTextAreaProps.safeParse(inp).success diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControl.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControl.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.lazy.tsx new file mode 100644 index 00000000..ca476642 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlContainerProps } from './FormControlContainer.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlContainer = lazy(async () => await import('./FormControlContainer')) + +const FormControlContainer = (props: JSX.IntrinsicAttributes & FormControlContainerProps): JSX.Element => ( + }> + + +) + +export default FormControlContainer diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.module.css new file mode 100644 index 00000000..30002005 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.stories.tsx new file mode 100644 index 00000000..f638f0c7 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlContainer from './FormControlContainer' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlContainer, + title: '03-Particles/FormControl/FormControlContainer', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlContainer' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.tsx new file mode 100644 index 00000000..3da26428 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.tsx @@ -0,0 +1,42 @@ +import { useMemo, type FC } from 'react' + +import clsx from 'clsx' + +import type { FormControlContainerProps } from './FormControlContainer.types' + +import { coerceErrorBoolean } from '@/lib/utils' + +import styles from './FormControlContainer.module.css' + +const FormControlContainer: FC = ({ + className, + children, + error, + hasFocus, + hasPreContent, + hasPostContent +}) => { + hasFocus ??= false + hasPreContent ??= false + hasPostContent ??= false + + const hasError = useMemo(() => coerceErrorBoolean(error), [error]) + + return ( +
+ {children} +
+ ) +} + +export default FormControlContainer diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.types.ts new file mode 100644 index 00000000..68a71a18 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.types.ts @@ -0,0 +1,11 @@ +import type { ReactNode } from 'react' + +import type { CastReactElement } from '@/types/utils' + +export interface FormControlContainerProps extends CastReactElement<'div'> { + children?: ReactNode + error?: unknown + hasFocus?: boolean + hasPreContent?: boolean + hasPostContent?: boolean +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlContainer/FormControlContainer.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.lazy.tsx new file mode 100644 index 00000000..145c824f --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlDefaultProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlDefault = lazy(async () => await import('./FormControlDefault')) + +const FormControlDefault = (props: JSX.IntrinsicAttributes & FormControlDefaultProps): JSX.Element => ( + }> + + +) + +export default FormControlDefault diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.module.css new file mode 100644 index 00000000..30002005 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.stories.tsx new file mode 100644 index 00000000..77f3aee8 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlDefault from './FormControlDefault' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlDefault, + title: '03-Particles/FormControl/FormControlDefault', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlDefault' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.tsx new file mode 100644 index 00000000..0ec26ba1 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.tsx @@ -0,0 +1,91 @@ +import { useMemo, useState, type FC } from 'react' + +import { XMarkIcon } from '@heroicons/react/16/solid' +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import type { FormControlDefaultProps } from '../FormControl.types' + +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import InfoButton from '@/components/05-materials/InfoButton/InfoButton' + +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlDefault.module.css' + +const FormControlDefault: FC = ({ + className, + error, + errorMessage, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + error ??= false + + formType ??= 'text' + + const hasPreContent = useMemo(() => preContent != null, [preContent]) + const hasPostContent = useMemo(() => infoButton != null, [infoButton]) + + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ + + {preContent ?? ''} + + { + setHasFocus(false) + void onBlur(event) + }} + onChange={(event) => { + void onChange(event) + }} + onFocus={() => { + setHasFocus(true) + }} + {...restProps} + {...formProps} + /> + + {formType !== 'password' && reset != null && ( + + { + if (reset != null) reset() + }} + /> + + )} + + + + + + + +
+ ) +} +export default FormControlDefault diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.types.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDefault/FormControlDefault.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.lazy.tsx new file mode 100644 index 00000000..228e9a86 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlDropdownProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlDropdown = lazy(async () => await import('./FormControlDropdown')) + +const FormControlDropdown = (props: JSX.IntrinsicAttributes & FormControlDropdownProps): JSX.Element => ( + }> + + +) + +export default FormControlDropdown diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.module.css new file mode 100644 index 00000000..30002005 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.stories.tsx new file mode 100644 index 00000000..7ff65c9a --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlDropdown from './FormControlDropdown' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlDropdown, + title: '03-Particles/FormControl/FormControlDropdown', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlDropdown' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.tsx new file mode 100644 index 00000000..012318d0 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.tsx @@ -0,0 +1,65 @@ +import { useState, type FC } from 'react' + +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import { isFormControlDropdownOption, type FormControlDropdownProps } from '../FormControl.types' +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlDropdown.module.css' + +const FormControlDropdown: FC = ({ + className, + error, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ + + +
+ ) +} + +export default FormControlDropdown diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.types.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlDropdown/FormControlDropdown.utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.lazy.tsx new file mode 100644 index 00000000..5835dac7 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlPinProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlPin = lazy(async () => await import('./FormControlPin')) + +const FormControlPin = (props: JSX.IntrinsicAttributes & FormControlPinProps): JSX.Element => ( + }> + + +) + +export default FormControlPin diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.module.css new file mode 100644 index 00000000..30002005 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.stories.tsx new file mode 100644 index 00000000..c207eaf2 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlPin from './FormControlPin' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlPin, + title: '03-Particles/FormControl/FormControlPin', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlPin' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.tsx new file mode 100644 index 00000000..6e63a8bb --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.tsx @@ -0,0 +1,78 @@ +import { useMemo, useState, type FC } from 'react' + +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import type { FormControlPinProps } from '../FormControl.types' + +import Conditional from '@/components/01-atoms/Conditional/Conditional' +import InfoButton from '@/components/05-materials/InfoButton/InfoButton' + +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlPin.module.css' +import { coercePaddedPinEvent } from './FormControlPin.utils' + +const FormControlPin: FC = ({ + className, + error, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + const hasPreContent = useMemo(() => preContent != null, [preContent]) + const hasPostContent = useMemo(() => infoButton != null, [infoButton]) + + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ + + {preContent ?? ''} + + { + void onBlur(coercePaddedPinEvent(event)) + setHasFocus(false) + }} + onChange={(event) => { + void onChange(coercePaddedPinEvent(event)) + }} + onFocus={() => { + setHasFocus(true) + }} + max={9999} + size={4} + maxLength={4} + {...restProps} + {...formProps} + /> + + + + + + +
+ ) +} +export default FormControlPin diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.types.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.types.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.utils.ts b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.utils.ts new file mode 100644 index 00000000..a6f605b5 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlPin/FormControlPin.utils.ts @@ -0,0 +1,13 @@ +import type { BaseSyntheticEvent, FocusEvent, ChangeEvent } from 'react' + +export const coercePaddedPinEvent = ( + event: ChangeEvent | FocusEvent +): BaseSyntheticEvent => { + const t = event.target.value.padStart(4, '0') + const v = t.slice(-4, t.length) + + const copyEvent = structuredClone(event) + copyEvent.target.value = v + + return copyEvent +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.lazy.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.lazy.tsx new file mode 100644 index 00000000..bbdc6d93 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.lazy.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense, type JSX } from 'react' + +import type { FormControlTextAreaProps } from '../FormControl.types' + +import LoadingOverlay from '@/components/03-particles/LoadingOverlay/LoadingOverlay' + +const LazyFormControlTextArea = lazy(async () => await import('./FormControlTextArea')) + +const FormControlTextArea = (props: JSX.IntrinsicAttributes & FormControlTextAreaProps): JSX.Element => ( + }> + + +) + +export default FormControlTextArea diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.module.css b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.module.css new file mode 100644 index 00000000..30002005 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.module.css @@ -0,0 +1 @@ +@import '../FormControl.module.css'; diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.stories.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.stories.tsx new file mode 100644 index 00000000..1b5a3a71 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AuthenticationProvider from '@/components/09-providers/AuthenticationProvider/AuthenticationProvider' + +import FormControlTextArea from './FormControlTextArea' + +type StoryType = StoryObj + +const meta: Meta = { + component: FormControlTextArea, + title: '03-Particles/FormControl/FormControlTextArea', + decorators: [ + (Story) => ( + + + + ) + ] +} + +export default meta + +export const Default: StoryType = { + args: { + children: 'FormControlTextArea' + } +} diff --git a/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.tsx b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.tsx new file mode 100644 index 00000000..621c8d51 --- /dev/null +++ b/packages/frontend-react/src/components/03-particles/FormControl/FormControlTextArea/FormControlTextArea.tsx @@ -0,0 +1,57 @@ +import { useState, type FC } from 'react' + +import clsx from 'clsx' +import { useFormContext } from 'react-hook-form' + +import type { FormControlTextAreaProps } from '../FormControl.types' + +import FormControlContainer from '../FormControlContainer/FormControlContainer' + +import styles from './FormControlTextArea.module.css' + +const FormControlTextArea: FC = ({ + className, + error, + formType, + formKey, + id, + infoButton, + options, + preContent, + reset, + ...restProps +}) => { + const [hasFocus, setHasFocus] = useState(false) + + const form = useFormContext() + + const { onBlur, onChange, ...formProps } = form.register(formKey) + + return ( +
+ +