diff --git a/assets/js/bottom-bar.js b/assets/js/bottom-bar.js index 1d311c29..57047e30 100644 --- a/assets/js/bottom-bar.js +++ b/assets/js/bottom-bar.js @@ -1,13 +1,15 @@ (function ($) { - 'use strict'; const $phone = $('#phone'); const $email = $('#email'); $('body').append(`
- ${$phone.attr('title')} - ${$email.attr('title')} + ${$phone.attr('title')} + ${$email.attr('title')}
`); - })(window.jQuery); diff --git a/assets/js/user/_sidebar.js b/assets/js/user/_sidebar.js index ce573b52..6e1f36c9 100644 --- a/assets/js/user/_sidebar.js +++ b/assets/js/user/_sidebar.js @@ -7,7 +7,9 @@ $('.list-group-item-action:eq(2)').addClass('active'); } else if (currentUrl.indexOf('unpublished') !== -1) { $('.list-group-item-action:eq(1)').addClass('active'); + } else if (currentUrl.indexOf('security') !== -1) { + $('.list-group-item-action:eq(3)').addClass('active'); } else { $('.list-group-item-action:eq(0)').addClass('active'); } -})($); +})(window.jQuery); diff --git a/assets/js/user/two_factor/google_authenticator.js b/assets/js/user/two_factor/google_authenticator.js new file mode 100644 index 00000000..3d03a2e1 --- /dev/null +++ b/assets/js/user/two_factor/google_authenticator.js @@ -0,0 +1,89 @@ +(function ($) { + 'use strict'; + + const $form = $('#generate_google_auth_secret'); + const token = $('[name="auth_token"]').val(); + const $secret = $('[name="generatedSecret"]'); + const $authentication_code = $('[name="authentication_code"]'); + + // Open modal window + $('#setUpAuthenticatorButton').click(function () { + if ($form.data('generate-new-secret') === true) { + $.ajax({ + method: 'GET', + url: '/en/user/google_authenticator_code', + data: { csrf_token: token } + }).done(function (response) { + const { secret, qr_code } = response; + const image = new Image(); + image.src = qr_code; + + $secret.val(secret); + $('#generatedQrCode').html(image); + $('#generatedSecret').html(secret); + }); + } + }); + + // Enable 2fa + $('#enable2fa').click(function () { + let authentication_code = $authentication_code.val().trim(); + + if (!authentication_code) { + $authentication_code.addClass('is-invalid').focus(); + + return; + } + + $.ajax({ + method: 'PUT', + url: $form.attr('action'), + data: { + csrf_token: token, + secret: $secret.val(), + authentication_code: authentication_code + } + }) + .done(function () { + location.reload(); + }) + .fail(function (data) { + showError(data.responseJSON.message); + }); + }); + + // Disable 2fa + $('#disable2fa').click(function () { + $.ajax({ + method: 'DELETE', + url: $form.attr('action'), + data: { csrf_token: token } + }) + .done(function () { + location.reload(); + }) + .fail(function () { + location.reload(); + }); + }); + + $authentication_code.keyup(function () { + $(this).removeClass('is-invalid'); + }); + + // Reset form state + $('#setUpAuthenticator').on('hidden.bs.modal', function () { + resetFormState(); + }); + + function resetFormState() { + $authentication_code.val('').removeClass('is-invalid'); + $('#twoFactorAuthErrorMessage').text('').addClass('d-none'); + } + + function showError(message) { + $('#twoFactorAuthErrorMessage') + .text(message) + .removeClass('d-none'); + } +})(window.jQuery); diff --git a/assets/styles/user/security.scss b/assets/styles/user/security.scss new file mode 100644 index 00000000..09853db5 --- /dev/null +++ b/assets/styles/user/security.scss @@ -0,0 +1,6 @@ +.security-buttons a i { + background-color: #fff; + padding: 20px; + border-radius: 50%; + color: #BDC3C7; +} diff --git a/composer.json b/composer.json index 83852693..3424a21f 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,12 @@ "doctrine/doctrine-bundle": "^2.8", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.13", + "endroid/qr-code": "^4.8", "gregwar/image": "master", "knplabs/knp-paginator-bundle": "^5.6", "phpdocumentor/reflection-docblock": "^5.2", + "scheb/2fa-bundle": "^6.8", + "scheb/2fa-google-authenticator": "^6.8", "symfony/asset": "^6.2", "symfony/cache": "^6.2", "symfony/console": "^6.2", @@ -59,6 +62,7 @@ "dbrekelmans/bdi": "^1.0", "doctrine/doctrine-fixtures-bundle": "^3.4", "friendsofphp/php-cs-fixer": "^3.2", + "phpgangsta/googleauthenticator": "dev-master", "phpunit/phpunit": "^9.5", "rector/rector": "^0.14.5", "symfony/browser-kit": "^6.2", diff --git a/composer.lock b/composer.lock index ebcede81..65a17451 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,112 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3c107b1a514de87fe65342b40036a85d", + "content-hash": "13208988da7d237ad05740d9e74749b9", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.1", + "phpunit/phpunit": "^7 | ^8 | ^9", + "spatie/phpunit-snapshot-assertions": "^4.2.9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + }, + "time": "2022-12-07T17:46:57+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", + "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 | ^8 | ^9", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.4" + }, + "time": "2023-03-01T18:44:03+00:00" + }, { "name": "doctrine/annotations", "version": "1.14.3", @@ -354,16 +458,16 @@ }, { "name": "doctrine/dbal", - "version": "3.6.1", + "version": "3.6.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e" + "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/57815c7bbcda3cd18871d253c1dd8cbe56f8526e", - "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/b4bd1cfbd2b916951696d82e57d054394d84864c", + "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c", "shasum": "" }, "require": { @@ -379,9 +483,9 @@ "doctrine/coding-standard": "11.1.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2022.3", - "phpstan/phpstan": "1.10.3", + "phpstan/phpstan": "1.10.9", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.4", + "phpunit/phpunit": "9.6.6", "psalm/plugin-phpunit": "0.18.4", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^5.4|^6.0", @@ -446,7 +550,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.1" + "source": "https://github.com/doctrine/dbal/tree/3.6.2" }, "funding": [ { @@ -462,7 +566,7 @@ "type": "tidelift" } ], - "time": "2023-03-02T19:26:24+00:00" + "time": "2023-04-14T07:25:38+00:00" }, { "name": "doctrine/deprecations", @@ -509,21 +613,21 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.8.3", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "fd67ba64db3c806f626a33dcab15a4db0c77652e" + "reference": "7539b3c8bd620f7df6c2c6d510204bd2ce0064e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/fd67ba64db3c806f626a33dcab15a4db0c77652e", - "reference": "fd67ba64db3c806f626a33dcab15a4db0c77652e", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/7539b3c8bd620f7df6c2c6d510204bd2ce0064e3", + "reference": "7539b3c8bd620f7df6c2c6d510204bd2ce0064e3", "shasum": "" }, "require": { "doctrine/cache": "^1.11 || ^2.0", - "doctrine/dbal": "^3.4.0", + "doctrine/dbal": "^3.6.0", "doctrine/persistence": "^2.2 || ^3", "doctrine/sql-formatter": "^1.0.1", "php": "^7.4 || ^8.0", @@ -539,11 +643,12 @@ "conflict": { "doctrine/annotations": ">=3.0", "doctrine/orm": "<2.11 || >=3.0", - "twig/twig": "<1.34 || >=2.0,<2.4" + "twig/twig": "<1.34 || >=2.0 <2.4" }, "require-dev": { "doctrine/annotations": "^1 || ^2", "doctrine/coding-standard": "^9.0", + "doctrine/deprecations": "^1.0", "doctrine/orm": "^2.11 || ^3.0", "friendsofphp/proxy-manager-lts": "^1.0", "phpunit/phpunit": "^9.5.26 || ^10.0", @@ -604,7 +709,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.8.3" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.9.1" }, "funding": [ { @@ -620,7 +725,7 @@ "type": "tidelift" } ], - "time": "2023-02-03T09:32:42+00:00" + "time": "2023-04-14T05:39:34+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -1141,16 +1246,16 @@ }, { "name": "doctrine/orm", - "version": "2.14.1", + "version": "2.14.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e" + "reference": "e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/de7eee5ed7b1b35c99b118f26f210a8281e6db8e", - "reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e", + "url": "https://api.github.com/repos/doctrine/orm/zipball/e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52", + "reference": "e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52", "shasum": "" }, "require": { @@ -1179,14 +1284,14 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^11.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.9.8", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpstan/phpstan": "~1.4.10 || 1.10.6", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.7.1", + "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.4.0" + "vimeo/psalm": "4.30.0 || 5.9.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1236,9 +1341,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.14.1" + "source": "https://github.com/doctrine/orm/tree/2.14.2" }, - "time": "2023-01-16T18:36:59+00:00" + "time": "2023-03-30T15:18:54+00:00" }, { "name": "doctrine/persistence", @@ -1457,6 +1562,81 @@ ], "time": "2023-01-14T14:17:03+00:00" }, + { + "name": "endroid/qr-code", + "version": "4.8.2", + "source": { + "type": "git", + "url": "https://github.com/endroid/qr-code.git", + "reference": "2436c2333a3931c95e2b96eb82f16f53143d6bba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/2436c2333a3931c95e2b96eb82f16f53143d6bba", + "reference": "2436c2333a3931c95e2b96eb82f16f53143d6bba", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^2.0.5", + "php": "^8.0" + }, + "conflict": { + "khanamiryan/qrcode-detector-decoder": "^1.0.6" + }, + "require-dev": { + "endroid/quality": "dev-master", + "ext-gd": "*", + "khanamiryan/qrcode-detector-decoder": "^1.0.4||^2.0.2", + "setasign/fpdf": "^1.8.2" + }, + "suggest": { + "ext-gd": "Enables you to write PNG images", + "khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator", + "roave/security-advisories": "Makes sure package versions with known security issues are not installed", + "setasign/fpdf": "Enables you to use the PDF writer" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl" + } + ], + "description": "Endroid QR Code", + "homepage": "https://github.com/endroid/qr-code", + "keywords": [ + "code", + "endroid", + "php", + "qr", + "qrcode" + ], + "support": { + "issues": "https://github.com/endroid/qr-code/issues", + "source": "https://github.com/endroid/qr-code/tree/4.8.2" + }, + "funding": [ + { + "url": "https://github.com/endroid", + "type": "github" + } + ], + "time": "2023-03-30T18:46:02+00:00" + }, { "name": "friendsofphp/proxy-manager-lts", "version": "v1.0.14", @@ -1804,29 +1984,29 @@ }, { "name": "laminas/laminas-code", - "version": "4.8.0", + "version": "4.10.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-code.git", - "reference": "dd19fe8e07cc3f374308565667eecd4958c22106" + "reference": "ad8b36073f9ac792716478befadca0798cc15635" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-code/zipball/dd19fe8e07cc3f374308565667eecd4958c22106", - "reference": "dd19fe8e07cc3f374308565667eecd4958c22106", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/ad8b36073f9ac792716478befadca0798cc15635", + "reference": "ad8b36073f9ac792716478befadca0798cc15635", "shasum": "" }, "require": { "php": "~8.1.0 || ~8.2.0" }, "require-dev": { - "doctrine/annotations": "^1.13.3", + "doctrine/annotations": "^2.0.0", "ext-phar": "*", "laminas/laminas-coding-standard": "^2.3.0", "laminas/laminas-stdlib": "^3.6.1", - "phpunit/phpunit": "^9.5.26", - "psalm/plugin-phpunit": "^0.18.0", - "vimeo/psalm": "^5.1.0" + "phpunit/phpunit": "^10.0.9", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.7.1" }, "suggest": { "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", @@ -1863,7 +2043,7 @@ "type": "community_bridge" } ], - "time": "2022-12-08T02:08:23+00:00" + "time": "2023-03-08T11:55:01+00:00" }, { "name": "monolog/monolog", @@ -1966,6 +2146,73 @@ ], "time": "2023-02-06T13:46:10+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "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": "2022-06-14T06:56:20+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -2078,24 +2325,27 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.2", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d" + "reference": "dfc078e8af9c99210337325ff5aa152872c98714" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/48f445a408c131e38cab1c235aa6d2bb7a0bb20d", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dfc078e8af9c99210337325ff5aa152872c98714", + "reference": "dfc078e8af9c99210337325ff5aa152872c98714", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.0", "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" }, "require-dev": { "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.1", @@ -2127,9 +2377,54 @@ "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.6.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.1" + }, + "time": "2023-03-27T19:02:04+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.20.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "6c04009f6cae6eda2f040745b6b846080ef069c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6c04009f6cae6eda2f040745b6b846080ef069c2", + "reference": "6c04009f6cae6eda2f040745b6b846080ef069c2", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "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/1.20.3" }, - "time": "2022-10-14T12:47:21+00:00" + "time": "2023-04-25T09:01:03+00:00" }, { "name": "psr/cache", @@ -2389,6 +2684,204 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "scheb/2fa-bundle", + "version": "v6.8.0", + "source": { + "type": "git", + "url": "https://github.com/scheb/2fa-bundle.git", + "reference": "4f8e9e87f90cf50c72b0857ea2b88453cf1d2446" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/4f8e9e87f90cf50c72b0857ea2b88453cf1d2446", + "reference": "4f8e9e87f90cf50c72b0857ea2b88453cf1d2446", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-foundation": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/property-access": "^5.4 || ^6.0", + "symfony/security-bundle": "^5.4 || ^6.0", + "symfony/twig-bundle": "^5.4 || ^6.0" + }, + "conflict": { + "scheb/two-factor-bundle": "*" + }, + "suggest": { + "scheb/2fa-backup-code": "Emergency codes when you have no access to other methods", + "scheb/2fa-email": "Send codes by email", + "scheb/2fa-google-authenticator": "Google Authenticator support", + "scheb/2fa-totp": "Temporary one-time password (TOTP) support (Google Authenticator compatible)", + "scheb/2fa-trusted-device": "Trusted devices support" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Scheb\\TwoFactorBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Scheb", + "email": "me@christianscheb.de" + } + ], + "description": "A generic interface to implement two-factor authentication in Symfony applications", + "homepage": "https://github.com/scheb/2fa", + "keywords": [ + "2fa", + "Authentication", + "symfony", + "two-factor", + "two-step" + ], + "support": { + "source": "https://github.com/scheb/2fa-bundle/tree/v6.8.0" + }, + "time": "2023-01-26T18:47:22+00:00" + }, + { + "name": "scheb/2fa-google-authenticator", + "version": "v6.8.0", + "source": { + "type": "git", + "url": "https://github.com/scheb/2fa-google-authenticator.git", + "reference": "20eab4c1814b587cac71c4516a06b192ca838294" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/20eab4c1814b587cac71c4516a06b192ca838294", + "reference": "20eab4c1814b587cac71c4516a06b192ca838294", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2.4", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "scheb/2fa-bundle": "self.version", + "spomky-labs/otphp": "^10.0 || ^11.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Scheb\\TwoFactorBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Scheb", + "email": "me@christianscheb.de" + } + ], + "description": "Extends scheb/2fa-bundle with two-factor authentication using Google Authenticator", + "homepage": "https://github.com/scheb/2fa", + "keywords": [ + "2fa", + "Authentication", + "google-authenticator", + "symfony", + "two-factor", + "two-step" + ], + "support": { + "source": "https://github.com/scheb/2fa-google-authenticator/tree/v6.8.0" + }, + "time": "2022-12-10T15:20:09+00:00" + }, + { + "name": "spomky-labs/otphp", + "version": "11.2.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "9a1569038bb1c8e98040b14b8bcbba54f25e7795" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/9a1569038bb1c8e98040b14b8bcbba54f25e7795", + "reference": "9a1569038bb1c8e98040b14b8bcbba54f25e7795", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "paragonie/constant_time_encoding": "^2.0", + "php": "^8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.26", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5.26", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^0.15", + "symfony/phpunit-bridge": "^6.1", + "symplify/easy-coding-standard": "^11.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OTPHP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + } + ], + "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator", + "homepage": "https://github.com/Spomky-Labs/otphp", + "keywords": [ + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/otphp/issues", + "source": "https://github.com/Spomky-Labs/otphp/tree/11.2.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2023-03-16T19:16:25+00:00" + }, { "name": "symfony/asset", "version": "v6.2.7", @@ -2463,16 +2956,16 @@ }, { "name": "symfony/cache", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "01a36b32f930018764bcbde006fbbe421fa6b61e" + "reference": "1ce7ed8e7ca6948892b6a3a52bb60cf2b04f7c94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/01a36b32f930018764bcbde006fbbe421fa6b61e", - "reference": "01a36b32f930018764bcbde006fbbe421fa6b61e", + "url": "https://api.github.com/repos/symfony/cache/zipball/1ce7ed8e7ca6948892b6a3a52bb60cf2b04f7c94", + "reference": "1ce7ed8e7ca6948892b6a3a52bb60cf2b04f7c94", "shasum": "" }, "require": { @@ -2481,7 +2974,7 @@ "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^1.1.7|^2|^3", "symfony/service-contracts": "^1.1|^2|^3", - "symfony/var-exporter": "^6.2.7" + "symfony/var-exporter": "^6.2.10" }, "conflict": { "doctrine/dbal": "<2.13.1", @@ -2539,7 +3032,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.2.7" + "source": "https://github.com/symfony/cache/tree/v6.2.10" }, "funding": [ { @@ -2555,7 +3048,7 @@ "type": "tidelift" } ], - "time": "2023-02-21T16:15:44+00:00" + "time": "2023-04-21T15:42:15+00:00" }, { "name": "symfony/cache-contracts", @@ -2715,16 +3208,16 @@ }, { "name": "symfony/console", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cbad09eb8925b6ad4fb721c7a179344dc4a19d45" + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cbad09eb8925b6ad4fb721c7a179344dc4a19d45", - "reference": "cbad09eb8925b6ad4fb721c7a179344dc4a19d45", + "url": "https://api.github.com/repos/symfony/console/zipball/12288d9f4500f84a4d02254d4aa968b15488476f", + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f", "shasum": "" }, "require": { @@ -2786,12 +3279,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.7" + "source": "https://github.com/symfony/console/tree/v6.2.10" }, "funding": [ { @@ -2807,20 +3300,20 @@ "type": "tidelift" } ], - "time": "2023-02-25T17:00:03+00:00" + "time": "2023-04-28T13:37:43+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "83369dd4ec84bba9673524d25b79dfbde9e6e84c" + "reference": "d732a66a2672669232c0b4536c8c96724a679780" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/83369dd4ec84bba9673524d25b79dfbde9e6e84c", - "reference": "83369dd4ec84bba9673524d25b79dfbde9e6e84c", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d732a66a2672669232c0b4536c8c96724a679780", + "reference": "d732a66a2672669232c0b4536c8c96724a679780", "shasum": "" }, "require": { @@ -2878,7 +3371,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.2.7" + "source": "https://github.com/symfony/dependency-injection/tree/v6.2.10" }, "funding": [ { @@ -2894,7 +3387,7 @@ "type": "tidelift" } ], - "time": "2023-02-16T14:11:02+00:00" + "time": "2023-04-21T15:42:15+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2965,16 +3458,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.2.7", + "version": "v6.2.9", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "35cb5045e15bf6bd89fd1353d9b03ff61dc1feaf" + "reference": "4b3aeaa90d41c5527d7ba211d12102cedf06936e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/35cb5045e15bf6bd89fd1353d9b03ff61dc1feaf", - "reference": "35cb5045e15bf6bd89fd1353d9b03ff61dc1feaf", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/4b3aeaa90d41c5527d7ba211d12102cedf06936e", + "reference": "4b3aeaa90d41c5527d7ba211d12102cedf06936e", "shasum": "" }, "require": { @@ -3060,7 +3553,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.2.7" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.2.9" }, "funding": [ { @@ -3076,20 +3569,20 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-04-11T16:08:35+00:00" }, { "name": "symfony/dotenv", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "f2b09b7ee21458779df000bd24020b6e9955b393" + "reference": "4481aa45be7a11d2335c1d5b5bbe2f0c6199b105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/f2b09b7ee21458779df000bd24020b6e9955b393", - "reference": "f2b09b7ee21458779df000bd24020b6e9955b393", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/4481aa45be7a11d2335c1d5b5bbe2f0c6199b105", + "reference": "4481aa45be7a11d2335c1d5b5bbe2f0c6199b105", "shasum": "" }, "require": { @@ -3134,7 +3627,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.2.7" + "source": "https://github.com/symfony/dotenv/tree/v6.2.8" }, "funding": [ { @@ -3150,20 +3643,20 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-03-10T10:06:03+00:00" }, { "name": "symfony/error-handler", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "61e90f94eb014054000bc902257d2763fac09166" + "reference": "8b7e9f124640cb0611624a9383176c3e5f7d8cfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/61e90f94eb014054000bc902257d2763fac09166", - "reference": "61e90f94eb014054000bc902257d2763fac09166", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8b7e9f124640cb0611624a9383176c3e5f7d8cfb", + "reference": "8b7e9f124640cb0611624a9383176c3e5f7d8cfb", "shasum": "" }, "require": { @@ -3205,7 +3698,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.2.7" + "source": "https://github.com/symfony/error-handler/tree/v6.2.10" }, "funding": [ { @@ -3221,20 +3714,20 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-04-18T13:46:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "404b307de426c1c488e5afad64403e5f145e82a5" + "reference": "04046f35fd7d72f9646e721fc2ecb8f9c67d3339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/404b307de426c1c488e5afad64403e5f145e82a5", - "reference": "404b307de426c1c488e5afad64403e5f145e82a5", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/04046f35fd7d72f9646e721fc2ecb8f9c67d3339", + "reference": "04046f35fd7d72f9646e721fc2ecb8f9c67d3339", "shasum": "" }, "require": { @@ -3288,7 +3781,7 @@ "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/v6.2.7" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.2.8" }, "funding": [ { @@ -3304,7 +3797,7 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-03-20T16:06:02+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -3450,16 +3943,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "82b6c62b959f642d000456f08c6d219d749215b3" + "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/82b6c62b959f642d000456f08c6d219d749215b3", - "reference": "82b6c62b959f642d000456f08c6d219d749215b3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd588debf7d1bc16a2c84b4b3b71145d9946b894", + "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894", "shasum": "" }, "require": { @@ -3493,7 +3986,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.2.7" + "source": "https://github.com/symfony/filesystem/tree/v6.2.10" }, "funding": [ { @@ -3509,7 +4002,7 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-04-18T13:46:08+00:00" }, { "name": "symfony/finder", @@ -3642,16 +4135,16 @@ }, { "name": "symfony/form", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "92379f5cf423c64e32be41897a65072ec972936e" + "reference": "a123512b46caea497ab8d96d9dbdbdaaf416a606" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/92379f5cf423c64e32be41897a65072ec972936e", - "reference": "92379f5cf423c64e32be41897a65072ec972936e", + "url": "https://api.github.com/repos/symfony/form/zipball/a123512b46caea497ab8d96d9dbdbdaaf416a606", + "reference": "a123512b46caea497ab8d96d9dbdbdaaf416a606", "shasum": "" }, "require": { @@ -3726,7 +4219,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v6.2.7" + "source": "https://github.com/symfony/form/tree/v6.2.10" }, "funding": [ { @@ -3742,20 +4235,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-19T08:03:37+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "01b1caa34ae121a192580acd38f66b7cb8b9ecce" + "reference": "823f285befde4e97bb70d97cae57997c38e4d6fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/01b1caa34ae121a192580acd38f66b7cb8b9ecce", - "reference": "01b1caa34ae121a192580acd38f66b7cb8b9ecce", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/823f285befde4e97bb70d97cae57997c38e4d6fd", + "reference": "823f285befde4e97bb70d97cae57997c38e4d6fd", "shasum": "" }, "require": { @@ -3764,7 +4257,7 @@ "php": ">=8.1", "symfony/cache": "^5.4|^6.0", "symfony/config": "^6.1", - "symfony/dependency-injection": "^6.2", + "symfony/dependency-injection": "^6.2.8", "symfony/deprecation-contracts": "^2.1|^3", "symfony/error-handler": "^6.1", "symfony/event-dispatcher": "^5.4|^6.0", @@ -3797,7 +4290,7 @@ "symfony/security-csrf": "<5.4", "symfony/serializer": "<6.1", "symfony/stopwatch": "<5.4", - "symfony/translation": "<5.4", + "symfony/translation": "<6.2.8", "symfony/twig-bridge": "<5.4", "symfony/twig-bundle": "<5.4", "symfony/validator": "<5.4", @@ -3832,7 +4325,7 @@ "symfony/serializer": "^6.1", "symfony/stopwatch": "^5.4|^6.0", "symfony/string": "^5.4|^6.0", - "symfony/translation": "^5.4|^6.0", + "symfony/translation": "^6.2.8", "symfony/twig-bundle": "^5.4|^6.0", "symfony/uid": "^5.4|^6.0", "symfony/validator": "^5.4|^6.0", @@ -3877,7 +4370,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.2.7" + "source": "https://github.com/symfony/framework-bundle/tree/v6.2.10" }, "funding": [ { @@ -3893,20 +4386,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-23T08:23:35+00:00" }, { "name": "symfony/google-mailer", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/google-mailer.git", - "reference": "de9ed9e23274151d8eb260d285d501e9bc0a0dbb" + "reference": "7e6cde8d40144e889e607bfc5320ea4192b247cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/google-mailer/zipball/de9ed9e23274151d8eb260d285d501e9bc0a0dbb", - "reference": "de9ed9e23274151d8eb260d285d501e9bc0a0dbb", + "url": "https://api.github.com/repos/symfony/google-mailer/zipball/7e6cde8d40144e889e607bfc5320ea4192b247cd", + "reference": "7e6cde8d40144e889e607bfc5320ea4192b247cd", "shasum": "" }, "require": { @@ -3942,7 +4435,7 @@ "description": "Symfony Google Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/google-mailer/tree/v6.2.7" + "source": "https://github.com/symfony/google-mailer/tree/v6.2.10" }, "funding": [ { @@ -3958,20 +4451,20 @@ "type": "tidelift" } ], - "time": "2023-02-21T10:35:38+00:00" + "time": "2023-04-14T16:23:31+00:00" }, { "name": "symfony/http-client", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "0a5be6cbc570ae23b51b49d67341f378629d78e4" + "reference": "3f5545a91c8e79dedd1a06c4b04e1682c80c42f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/0a5be6cbc570ae23b51b49d67341f378629d78e4", - "reference": "0a5be6cbc570ae23b51b49d67341f378629d78e4", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3f5545a91c8e79dedd1a06c4b04e1682c80c42f9", + "reference": "3f5545a91c8e79dedd1a06c4b04e1682c80c42f9", "shasum": "" }, "require": { @@ -4026,8 +4519,11 @@ ], "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", + "keywords": [ + "http" + ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.2.7" + "source": "https://github.com/symfony/http-client/tree/v6.2.10" }, "funding": [ { @@ -4043,7 +4539,7 @@ "type": "tidelift" } ], - "time": "2023-02-21T10:54:55+00:00" + "time": "2023-04-20T13:12:48+00:00" }, { "name": "symfony/http-client-contracts", @@ -4128,16 +4624,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "5fc3038d4a594223f9ea42e4e985548f3fcc9a3b" + "reference": "49adbb92bcb4e3c2943719d2756271e8b9602acc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5fc3038d4a594223f9ea42e4e985548f3fcc9a3b", - "reference": "5fc3038d4a594223f9ea42e4e985548f3fcc9a3b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49adbb92bcb4e3c2943719d2756271e8b9602acc", + "reference": "49adbb92bcb4e3c2943719d2756271e8b9602acc", "shasum": "" }, "require": { @@ -4186,7 +4682,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.2.7" + "source": "https://github.com/symfony/http-foundation/tree/v6.2.10" }, "funding": [ { @@ -4202,20 +4698,20 @@ "type": "tidelift" } ], - "time": "2023-02-21T10:54:55+00:00" + "time": "2023-04-18T13:46:08+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ca0680ad1e2d678536cc20e0ae33f9e4e5d2becd" + "reference": "81064a65a5496f17d2b6984f6519406f98864215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ca0680ad1e2d678536cc20e0ae33f9e4e5d2becd", - "reference": "ca0680ad1e2d678536cc20e0ae33f9e4e5d2becd", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/81064a65a5496f17d2b6984f6519406f98864215", + "reference": "81064a65a5496f17d2b6984f6519406f98864215", "shasum": "" }, "require": { @@ -4297,7 +4793,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.2.7" + "source": "https://github.com/symfony/http-kernel/tree/v6.2.10" }, "funding": [ { @@ -4313,20 +4809,20 @@ "type": "tidelift" } ], - "time": "2023-02-28T13:26:41+00:00" + "time": "2023-04-28T13:50:28+00:00" }, { "name": "symfony/intl", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "e7346ea6d88ae22e1b5d489b7a60135e72527cec" + "reference": "860c99e53149d22df1900d3aefdaeb17adb7669d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/e7346ea6d88ae22e1b5d489b7a60135e72527cec", - "reference": "e7346ea6d88ae22e1b5d489b7a60135e72527cec", + "url": "https://api.github.com/repos/symfony/intl/zipball/860c99e53149d22df1900d3aefdaeb17adb7669d", + "reference": "860c99e53149d22df1900d3aefdaeb17adb7669d", "shasum": "" }, "require": { @@ -4367,7 +4863,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a PHP replacement layer for the C intl extension that includes additional data from the ICU library", + "description": "Provides access to the localization data of the ICU library", "homepage": "https://symfony.com", "keywords": [ "i18n", @@ -4378,7 +4874,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.2.7" + "source": "https://github.com/symfony/intl/tree/v6.2.10" }, "funding": [ { @@ -4394,20 +4890,20 @@ "type": "tidelift" } ], - "time": "2023-02-21T10:54:55+00:00" + "time": "2023-04-14T16:23:31+00:00" }, { "name": "symfony/lock", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/lock.git", - "reference": "febdeed9473e568ff34bf4350c04760f5357dfe2" + "reference": "fe452788cc81762f0840bd2a3dd1f230193186e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/lock/zipball/febdeed9473e568ff34bf4350c04760f5357dfe2", - "reference": "febdeed9473e568ff34bf4350c04760f5357dfe2", + "url": "https://api.github.com/repos/symfony/lock/zipball/fe452788cc81762f0840bd2a3dd1f230193186e5", + "reference": "fe452788cc81762f0840bd2a3dd1f230193186e5", "shasum": "" }, "require": { @@ -4456,7 +4952,7 @@ "semaphore" ], "support": { - "source": "https://github.com/symfony/lock/tree/v6.2.7" + "source": "https://github.com/symfony/lock/tree/v6.2.8" }, "funding": [ { @@ -4472,20 +4968,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-03-14T15:00:05+00:00" }, { "name": "symfony/mailer", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "e4f84c633b72ec70efc50b8016871c3bc43e691e" + "reference": "bfcfa015c67e19c6fdb7ca6fe70700af1e740a17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/e4f84c633b72ec70efc50b8016871c3bc43e691e", - "reference": "e4f84c633b72ec70efc50b8016871c3bc43e691e", + "url": "https://api.github.com/repos/symfony/mailer/zipball/bfcfa015c67e19c6fdb7ca6fe70700af1e740a17", + "reference": "bfcfa015c67e19c6fdb7ca6fe70700af1e740a17", "shasum": "" }, "require": { @@ -4535,7 +5031,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.2.7" + "source": "https://github.com/symfony/mailer/tree/v6.2.8" }, "funding": [ { @@ -4551,20 +5047,20 @@ "type": "tidelift" } ], - "time": "2023-02-21T10:35:38+00:00" + "time": "2023-03-14T15:00:05+00:00" }, { "name": "symfony/messenger", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "5ca618865eb726354e5369dda38770af1a8a8331" + "reference": "f54eef78d500309bbc51291f8a353b4b0afef8c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/5ca618865eb726354e5369dda38770af1a8a8331", - "reference": "5ca618865eb726354e5369dda38770af1a8a8331", + "url": "https://api.github.com/repos/symfony/messenger/zipball/f54eef78d500309bbc51291f8a353b4b0afef8c1", + "reference": "f54eef78d500309bbc51291f8a353b4b0afef8c1", "shasum": "" }, "require": { @@ -4622,7 +5118,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v6.2.7" + "source": "https://github.com/symfony/messenger/tree/v6.2.8" }, "funding": [ { @@ -4638,20 +5134,20 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:57:23+00:00" + "time": "2023-03-14T15:00:05+00:00" }, { "name": "symfony/mime", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "62e341f80699badb0ad70b31149c8df89a2d778e" + "reference": "b6c137fc53a9f7c4c951cd3f362b3734c7a97723" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/62e341f80699badb0ad70b31149c8df89a2d778e", - "reference": "62e341f80699badb0ad70b31149c8df89a2d778e", + "url": "https://api.github.com/repos/symfony/mime/zipball/b6c137fc53a9f7c4c951cd3f362b3734c7a97723", + "reference": "b6c137fc53a9f7c4c951cd3f362b3734c7a97723", "shasum": "" }, "require": { @@ -4705,7 +5201,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.2.7" + "source": "https://github.com/symfony/mime/tree/v6.2.10" }, "funding": [ { @@ -4721,20 +5217,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-19T09:54:16+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "c611e34f9e7b075f3af6d89d7cd10c6a80ab6a74" + "reference": "34700f2e5c7e9eae78f8e59fc02399dd8f110cae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/c611e34f9e7b075f3af6d89d7cd10c6a80ab6a74", - "reference": "c611e34f9e7b075f3af6d89d7cd10c6a80ab6a74", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/34700f2e5c7e9eae78f8e59fc02399dd8f110cae", + "reference": "34700f2e5c7e9eae78f8e59fc02399dd8f110cae", "shasum": "" }, "require": { @@ -4788,7 +5284,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v6.2.7" + "source": "https://github.com/symfony/monolog-bridge/tree/v6.2.8" }, "funding": [ { @@ -4804,7 +5300,7 @@ "type": "tidelift" } ], - "time": "2023-02-21T16:15:44+00:00" + "time": "2023-03-09T16:20:02+00:00" }, { "name": "symfony/monolog-bundle", @@ -4889,16 +5385,16 @@ }, { "name": "symfony/notifier", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/notifier.git", - "reference": "051bb72b2e2b64dbdc1a0b4f70fb2a0050d96850" + "reference": "993df4464c577e7eb828324181d2a451e81619b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/notifier/zipball/051bb72b2e2b64dbdc1a0b4f70fb2a0050d96850", - "reference": "051bb72b2e2b64dbdc1a0b4f70fb2a0050d96850", + "url": "https://api.github.com/repos/symfony/notifier/zipball/993df4464c577e7eb828324181d2a451e81619b6", + "reference": "993df4464c577e7eb828324181d2a451e81619b6", "shasum": "" }, "require": { @@ -4945,7 +5441,7 @@ "notifier" ], "support": { - "source": "https://github.com/symfony/notifier/tree/v6.2.7" + "source": "https://github.com/symfony/notifier/tree/v6.2.8" }, "funding": [ { @@ -4961,7 +5457,7 @@ "type": "tidelift" } ], - "time": "2023-02-17T11:05:34+00:00" + "time": "2023-03-10T10:06:03+00:00" }, { "name": "symfony/options-resolver", @@ -5526,16 +6022,16 @@ }, { "name": "symfony/process", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "680e8a2ea6b3f87aecc07a6a65a203ae573d1902" + "reference": "b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/680e8a2ea6b3f87aecc07a6a65a203ae573d1902", - "reference": "680e8a2ea6b3f87aecc07a6a65a203ae573d1902", + "url": "https://api.github.com/repos/symfony/process/zipball/b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e", + "reference": "b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e", "shasum": "" }, "require": { @@ -5567,7 +6063,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.2.7" + "source": "https://github.com/symfony/process/tree/v6.2.10" }, "funding": [ { @@ -5583,20 +6079,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-18T13:56:57+00:00" }, { "name": "symfony/property-access", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "5a389172011e2c37b47c896d0b156549126690a1" + "reference": "2ad1e0a07b8cab3e09905659d14f3b248e916374" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/5a389172011e2c37b47c896d0b156549126690a1", - "reference": "5a389172011e2c37b47c896d0b156549126690a1", + "url": "https://api.github.com/repos/symfony/property-access/zipball/2ad1e0a07b8cab3e09905659d14f3b248e916374", + "reference": "2ad1e0a07b8cab3e09905659d14f3b248e916374", "shasum": "" }, "require": { @@ -5643,11 +6139,11 @@ "injection", "object", "property", - "property path", + "property-path", "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.2.7" + "source": "https://github.com/symfony/property-access/tree/v6.2.8" }, "funding": [ { @@ -5663,20 +6159,20 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-03-14T15:00:05+00:00" }, { "name": "symfony/property-info", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "5cf906918ea0f74032ffc5c0b85def246ce409df" + "reference": "617177c24e1a92e011851948ba973758429a68b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/5cf906918ea0f74032ffc5c0b85def246ce409df", - "reference": "5cf906918ea0f74032ffc5c0b85def246ce409df", + "url": "https://api.github.com/repos/symfony/property-info/zipball/617177c24e1a92e011851948ba973758429a68b2", + "reference": "617177c24e1a92e011851948ba973758429a68b2", "shasum": "" }, "require": { @@ -5736,7 +6232,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v6.2.7" + "source": "https://github.com/symfony/property-info/tree/v6.2.10" }, "funding": [ { @@ -5752,7 +6248,7 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:53:37+00:00" + "time": "2023-04-18T13:46:08+00:00" }, { "name": "symfony/proxy-manager-bridge", @@ -5895,16 +6391,16 @@ }, { "name": "symfony/routing", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "fa643fa4c56de161f8bc8c0492a76a60140b50e4" + "reference": "69062e2823f03b82265d73a966999660f0e1e404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/fa643fa4c56de161f8bc8c0492a76a60140b50e4", - "reference": "fa643fa4c56de161f8bc8c0492a76a60140b50e4", + "url": "https://api.github.com/repos/symfony/routing/zipball/69062e2823f03b82265d73a966999660f0e1e404", + "reference": "69062e2823f03b82265d73a966999660f0e1e404", "shasum": "" }, "require": { @@ -5963,7 +6459,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.2.7" + "source": "https://github.com/symfony/routing/tree/v6.2.8" }, "funding": [ { @@ -5979,20 +6475,20 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:53:37+00:00" + "time": "2023-03-14T15:00:05+00:00" }, { "name": "symfony/runtime", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "111b9d617d0cfc71d44baf01eb9951517fd8b739" + "reference": "f8b0751b33888329be8f8f0481bb81d279ec4157" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/111b9d617d0cfc71d44baf01eb9951517fd8b739", - "reference": "111b9d617d0cfc71d44baf01eb9951517fd8b739", + "url": "https://api.github.com/repos/symfony/runtime/zipball/f8b0751b33888329be8f8f0481bb81d279ec4157", + "reference": "f8b0751b33888329be8f8f0481bb81d279ec4157", "shasum": "" }, "require": { @@ -6038,8 +6534,11 @@ ], "description": "Enables decoupling PHP applications from global state", "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.2.7" + "source": "https://github.com/symfony/runtime/tree/v6.2.8" }, "funding": [ { @@ -6055,20 +6554,20 @@ "type": "tidelift" } ], - "time": "2023-02-02T07:44:01+00:00" + "time": "2023-03-14T15:48:35+00:00" }, { "name": "symfony/security-bundle", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "601bcc14b6e8c168dc5985d31cfdfd11bd07b50b" + "reference": "b12dcedbcf423ae6d34d79cfaa6791a21c90bd14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/601bcc14b6e8c168dc5985d31cfdfd11bd07b50b", - "reference": "601bcc14b6e8c168dc5985d31cfdfd11bd07b50b", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/b12dcedbcf423ae6d34d79cfaa6791a21c90bd14", + "reference": "b12dcedbcf423ae6d34d79cfaa6791a21c90bd14", "shasum": "" }, "require": { @@ -6083,7 +6582,7 @@ "symfony/password-hasher": "^5.4|^6.0", "symfony/security-core": "^6.2", "symfony/security-csrf": "^5.4|^6.0", - "symfony/security-http": "^6.2.6" + "symfony/security-http": "^6.2.10" }, "conflict": { "symfony/browser-kit": "<5.4", @@ -6139,7 +6638,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.2.7" + "source": "https://github.com/symfony/security-bundle/tree/v6.2.10" }, "funding": [ { @@ -6155,20 +6654,20 @@ "type": "tidelift" } ], - "time": "2023-02-21T12:32:47+00:00" + "time": "2023-04-21T15:49:06+00:00" }, { "name": "symfony/security-core", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "5dd5509ec58bf30c98811681870f58e7f5918bbe" + "reference": "c141337bc7451f9a9e464733f1e536bf38d1d2fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/5dd5509ec58bf30c98811681870f58e7f5918bbe", - "reference": "5dd5509ec58bf30c98811681870f58e7f5918bbe", + "url": "https://api.github.com/repos/symfony/security-core/zipball/c141337bc7451f9a9e464733f1e536bf38d1d2fb", + "reference": "c141337bc7451f9a9e464733f1e536bf38d1d2fb", "shasum": "" }, "require": { @@ -6230,7 +6729,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.2.7" + "source": "https://github.com/symfony/security-core/tree/v6.2.8" }, "funding": [ { @@ -6246,7 +6745,7 @@ "type": "tidelift" } ], - "time": "2023-02-17T11:05:34+00:00" + "time": "2023-03-10T10:06:03+00:00" }, { "name": "symfony/security-csrf", @@ -6321,16 +6820,16 @@ }, { "name": "symfony/security-http", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "0b96e76243877b53e9ff1418f9e538ecf480dd69" + "reference": "c468f059fac27680acf7e84cea07ba5ffff8942a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/0b96e76243877b53e9ff1418f9e538ecf480dd69", - "reference": "0b96e76243877b53e9ff1418f9e538ecf480dd69", + "url": "https://api.github.com/repos/symfony/security-http/zipball/c468f059fac27680acf7e84cea07ba5ffff8942a", + "reference": "c468f059fac27680acf7e84cea07ba5ffff8942a", "shasum": "" }, "require": { @@ -6386,7 +6885,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.2.7" + "source": "https://github.com/symfony/security-http/tree/v6.2.10" }, "funding": [ { @@ -6402,20 +6901,20 @@ "type": "tidelift" } ], - "time": "2023-02-28T10:56:03+00:00" + "time": "2023-04-21T11:56:14+00:00" }, { "name": "symfony/serializer", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "df9599873fdc2540e6f4291f49be4fcc167e9cbf" + "reference": "0732edf0ad28dd3faacde4f1200ab9d7a4d5f40d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/df9599873fdc2540e6f4291f49be4fcc167e9cbf", - "reference": "df9599873fdc2540e6f4291f49be4fcc167e9cbf", + "url": "https://api.github.com/repos/symfony/serializer/zipball/0732edf0ad28dd3faacde4f1200ab9d7a4d5f40d", + "reference": "0732edf0ad28dd3faacde4f1200ab9d7a4d5f40d", "shasum": "" }, "require": { @@ -6425,7 +6924,7 @@ "conflict": { "doctrine/annotations": "<1.12", "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0|>=1.7.0", + "phpdocumentor/type-resolver": "<1.4.0", "symfony/dependency-injection": "<5.4", "symfony/property-access": "<5.4", "symfony/property-info": "<5.4", @@ -6487,7 +6986,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.2.7" + "source": "https://github.com/symfony/serializer/tree/v6.2.10" }, "funding": [ { @@ -6503,7 +7002,7 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-18T13:57:49+00:00" }, { "name": "symfony/service-contracts", @@ -6654,16 +7153,16 @@ }, { "name": "symfony/string", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "67b8c1eec78296b85dc1c7d9743830160218993d" + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/67b8c1eec78296b85dc1c7d9743830160218993d", - "reference": "67b8c1eec78296b85dc1c7d9743830160218993d", + "url": "https://api.github.com/repos/symfony/string/zipball/193e83bbd6617d6b2151c37fff10fa7168ebddef", + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef", "shasum": "" }, "require": { @@ -6720,7 +7219,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.7" + "source": "https://github.com/symfony/string/tree/v6.2.8" }, "funding": [ { @@ -6736,20 +7235,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-03-20T16:06:02+00:00" }, { "name": "symfony/translation", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "90db1c6138c90527917671cd9ffa9e8b359e3a73" + "reference": "817535dbb1721df8b3a8f2489dc7e50bcd6209b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/90db1c6138c90527917671cd9ffa9e8b359e3a73", - "reference": "90db1c6138c90527917671cd9ffa9e8b359e3a73", + "url": "https://api.github.com/repos/symfony/translation/zipball/817535dbb1721df8b3a8f2489dc7e50bcd6209b5", + "reference": "817535dbb1721df8b3a8f2489dc7e50bcd6209b5", "shasum": "" }, "require": { @@ -6818,7 +7317,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.2.7" + "source": "https://github.com/symfony/translation/tree/v6.2.8" }, "funding": [ { @@ -6834,7 +7333,7 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-03-31T09:14:44+00:00" }, { "name": "symfony/translation-contracts", @@ -6919,16 +7418,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v6.2.7", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "f1899fd3b8a29f9544440a716a1ed7d1121c4615" + "reference": "30e3ad6ae749b2d2700ecf9b4a1a9d5c96b18927" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/f1899fd3b8a29f9544440a716a1ed7d1121c4615", - "reference": "f1899fd3b8a29f9544440a716a1ed7d1121c4615", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/30e3ad6ae749b2d2700ecf9b4a1a9d5c96b18927", + "reference": "30e3ad6ae749b2d2700ecf9b4a1a9d5c96b18927", "shasum": "" }, "require": { @@ -7023,7 +7522,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v6.2.7" + "source": "https://github.com/symfony/twig-bridge/tree/v6.2.8" }, "funding": [ { @@ -7039,7 +7538,7 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-03-31T09:14:44+00:00" }, { "name": "symfony/twig-bundle", @@ -7128,16 +7627,16 @@ }, { "name": "symfony/validator", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "4b3bd0a9545bdf7ebc84f0a494c05219010bb403" + "reference": "c02ea86844926f04247bc1f5db5f85bb53330823" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/4b3bd0a9545bdf7ebc84f0a494c05219010bb403", - "reference": "4b3bd0a9545bdf7ebc84f0a494c05219010bb403", + "url": "https://api.github.com/repos/symfony/validator/zipball/c02ea86844926f04247bc1f5db5f85bb53330823", + "reference": "c02ea86844926f04247bc1f5db5f85bb53330823", "shasum": "" }, "require": { @@ -7216,7 +7715,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.2.7" + "source": "https://github.com/symfony/validator/tree/v6.2.10" }, "funding": [ { @@ -7232,20 +7731,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-19T09:54:16+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "cf8d4ca1ddc1e3cc242375deb8fc23e54f5e2a1e" + "reference": "41a750a23412ca76fdbbf5096943b4134272c1ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cf8d4ca1ddc1e3cc242375deb8fc23e54f5e2a1e", - "reference": "cf8d4ca1ddc1e3cc242375deb8fc23e54f5e2a1e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41a750a23412ca76fdbbf5096943b4134272c1ab", + "reference": "41a750a23412ca76fdbbf5096943b4134272c1ab", "shasum": "" }, "require": { @@ -7304,7 +7803,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.2.7" + "source": "https://github.com/symfony/var-dumper/tree/v6.2.10" }, "funding": [ { @@ -7320,20 +7819,20 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-18T13:46:08+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "86062dd0103530e151588c8f60f5b85a139f1442" + "reference": "9a07920c2058bafee921ce4d90aeef2193837d63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/86062dd0103530e151588c8f60f5b85a139f1442", - "reference": "86062dd0103530e151588c8f60f5b85a139f1442", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/9a07920c2058bafee921ce4d90aeef2193837d63", + "reference": "9a07920c2058bafee921ce4d90aeef2193837d63", "shasum": "" }, "require": { @@ -7373,12 +7872,12 @@ "export", "hydrate", "instantiate", - "lazy loading", + "lazy-loading", "proxy", "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.2.7" + "source": "https://github.com/symfony/var-exporter/tree/v6.2.10" }, "funding": [ { @@ -7394,7 +7893,7 @@ "type": "tidelift" } ], - "time": "2023-02-24T10:42:00+00:00" + "time": "2023-04-21T08:33:05+00:00" }, { "name": "symfony/web-link", @@ -7557,16 +8056,16 @@ }, { "name": "symfony/yaml", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e8e6a1d59e050525f27a1f530aa9703423cb7f57" + "reference": "61916f3861b1e9705b18cfde723921a71dd1559d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e8e6a1d59e050525f27a1f530aa9703423cb7f57", - "reference": "e8e6a1d59e050525f27a1f530aa9703423cb7f57", + "url": "https://api.github.com/repos/symfony/yaml/zipball/61916f3861b1e9705b18cfde723921a71dd1559d", + "reference": "61916f3861b1e9705b18cfde723921a71dd1559d", "shasum": "" }, "require": { @@ -7611,7 +8110,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.2.7" + "source": "https://github.com/symfony/yaml/tree/v6.2.10" }, "funding": [ { @@ -7627,7 +8126,7 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:57:23+00:00" + "time": "2023-04-28T13:25:36+00:00" }, { "name": "symfonycasts/verify-email-bundle", @@ -8375,20 +8874,21 @@ }, { "name": "doctrine/data-fixtures", - "version": "1.6.3", + "version": "1.6.5", "source": { "type": "git", "url": "https://github.com/doctrine/data-fixtures.git", - "reference": "c27821d038e64f1bfc852a94064d65d2a75ad01f" + "reference": "e6b97f557942ea17564bbc30ae3ebc9bd2209363" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/c27821d038e64f1bfc852a94064d65d2a75ad01f", - "reference": "c27821d038e64f1bfc852a94064d65d2a75ad01f", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/e6b97f557942ea17564bbc30ae3ebc9bd2209363", + "reference": "e6b97f557942ea17564bbc30ae3ebc9bd2209363", "shasum": "" }, "require": { - "doctrine/persistence": "^1.3.3|^2.0|^3.0", + "doctrine/deprecations": "^0.5.3 || ^1.0", + "doctrine/persistence": "^1.3.3 || ^2.0 || ^3.0", "php": "^7.2 || ^8.0" }, "conflict": { @@ -8397,16 +8897,15 @@ "doctrine/phpcr-odm": "<1.3.0" }, "require-dev": { - "doctrine/coding-standard": "^10.0", + "doctrine/coding-standard": "^11.0", "doctrine/dbal": "^2.13 || ^3.0", - "doctrine/deprecations": "^1.0", "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", "doctrine/orm": "^2.12", "ext-sqlite3": "*", "phpstan/phpstan": "^1.5", - "phpunit/phpunit": "^8.5 || ^9.5", + "phpunit/phpunit": "^8.5 || ^9.5 || ^10.0", "symfony/cache": "^5.0 || ^6.0", - "vimeo/psalm": "^4.10" + "vimeo/psalm": "^4.10 || ^5.9" }, "suggest": { "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", @@ -8417,7 +8916,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\DataFixtures\\": "lib/Doctrine/Common/DataFixtures" + "Doctrine\\Common\\DataFixtures\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -8437,7 +8936,7 @@ ], "support": { "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/1.6.3" + "source": "https://github.com/doctrine/data-fixtures/tree/1.6.5" }, "funding": [ { @@ -8453,20 +8952,20 @@ "type": "tidelift" } ], - "time": "2023-01-07T15:10:22+00:00" + "time": "2023-04-03T14:58:58+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5" + "reference": "fd39829fed8f090ef6e185d33449d47c2fb59c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/601988c5b46dbd20a0f886f967210aba378a6fd5", - "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/fd39829fed8f090ef6e185d33449d47c2fb59c9c", + "reference": "fd39829fed8f090ef6e185d33449d47c2fb59c9c", "shasum": "" }, "require": { @@ -8520,7 +9019,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.2" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.3" }, "funding": [ { @@ -8536,7 +9035,7 @@ "type": "tidelift" } ], - "time": "2022-04-28T17:58:29+00:00" + "time": "2023-04-11T12:37:36+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -8630,26 +9129,24 @@ }, { "name": "masterminds/html5", - "version": "2.7.6", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947" + "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", + "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-dom": "*", - "ext-libxml": "*", "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8" }, "type": "library", "extra": { @@ -8693,22 +9190,22 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" + "source": "https://github.com/Masterminds/html5-php/tree/2.8.0" }, - "time": "2022-08-18T16:18:26+00:00" + "time": "2023-04-26T07:27:39+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -8746,7 +9243,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -8754,20 +9251,20 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", - "version": "v4.15.3", + "version": "v4.15.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", "shasum": "" }, "require": { @@ -8808,9 +9305,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" }, - "time": "2023-01-16T22:05:37+00:00" + "time": "2023-03-05T19:49:14+00:00" }, { "name": "phar-io/manifest", @@ -8989,18 +9486,66 @@ }, "time": "2023-02-09T12:12:19+00:00" }, + { + "name": "phpgangsta/googleauthenticator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/PHPGangsta/GoogleAuthenticator.git", + "reference": "505c2af8337b559b33557f37cda38e5f843f3768" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPGangsta/GoogleAuthenticator/zipball/505c2af8337b559b33557f37cda38e5f843f3768", + "reference": "505c2af8337b559b33557f37cda38e5f843f3768", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "default-branch": true, + "type": "library", + "autoload": { + "classmap": [ + "PHPGangsta/GoogleAuthenticator.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-4-Clause" + ], + "authors": [ + { + "name": "Michael Kliewe", + "email": "info@phpgangsta.de", + "homepage": "http://www.phpgangsta.de/", + "role": "Developer" + } + ], + "description": "Google Authenticator 2-factor authentication", + "keywords": [ + "googleauthenticator", + "rfc6238", + "totp" + ], + "support": { + "issues": "https://github.com/PHPGangsta/GoogleAuthenticator/issues", + "source": "https://github.com/PHPGangsta/GoogleAuthenticator" + }, + "time": "2019-03-20T00:55:58+00:00" + }, { "name": "phpstan/phpstan", - "version": "1.10.3", + "version": "1.10.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "5419375b5891add97dc74be71e6c1c34baaddf64" + "reference": "f07bf8c6980b81bf9e49d44bd0caf2e737614a70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5419375b5891add97dc74be71e6c1c34baaddf64", - "reference": "5419375b5891add97dc74be71e6c1c34baaddf64", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f07bf8c6980b81bf9e49d44bd0caf2e737614a70", + "reference": "f07bf8c6980b81bf9e49d44bd0caf2e737614a70", "shasum": "" }, "require": { @@ -9029,8 +9574,11 @@ "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", - "source": "https://github.com/phpstan/phpstan/tree/1.10.3" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -9046,20 +9594,20 @@ "type": "tidelift" } ], - "time": "2023-02-25T14:47:13+00:00" + "time": "2023-04-12T19:29:52+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.25", + "version": "9.2.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954" + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0e2b40518197a8c0d4b08bc34dfff1c99c508954", - "reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", "shasum": "" }, "require": { @@ -9081,8 +9629,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "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": { @@ -9115,7 +9663,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.25" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" }, "funding": [ { @@ -9123,7 +9671,7 @@ "type": "github" } ], - "time": "2023-02-25T05:32:00+00:00" + "time": "2023-03-06T12:58:08+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9368,16 +9916,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.4", + "version": "9.6.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9125ee085b6d95e78277dc07aa1f46f9e0607b8d" + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9125ee085b6d95e78277dc07aa1f46f9e0607b8d", - "reference": "9125ee085b6d95e78277dc07aa1f46f9e0607b8d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", "shasum": "" }, "require": { @@ -9410,8 +9958,8 @@ "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -9450,7 +9998,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.4" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" }, "funding": [ { @@ -9466,7 +10015,7 @@ "type": "tidelift" } ], - "time": "2023-02-27T13:06:37+00:00" + "time": "2023-04-14T08:58:40+00:00" }, { "name": "rector/rector", @@ -10705,16 +11254,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v6.2.7", + "version": "v6.2.9", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "65a906f5141ff2d3aef1b01c128fba78f5e9f921" + "reference": "328bc3795059651d2d4e462e8febdf7ec2d7a626" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/65a906f5141ff2d3aef1b01c128fba78f5e9f921", - "reference": "65a906f5141ff2d3aef1b01c128fba78f5e9f921", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/328bc3795059651d2d4e462e8febdf7ec2d7a626", + "reference": "328bc3795059651d2d4e462e8febdf7ec2d7a626", "shasum": "" }, "require": { @@ -10755,7 +11304,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.2.7" + "source": "https://github.com/symfony/dom-crawler/tree/v6.2.9" }, "funding": [ { @@ -10771,7 +11320,7 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-04-11T16:03:19+00:00" }, { "name": "symfony/maker-bundle", @@ -10957,16 +11506,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "56965fae0b6b8d271015990eff5240ffff02e185" + "reference": "552950db2919421ad917e29e76d1999a2a31a8e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/56965fae0b6b8d271015990eff5240ffff02e185", - "reference": "56965fae0b6b8d271015990eff5240ffff02e185", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/552950db2919421ad917e29e76d1999a2a31a8e3", + "reference": "552950db2919421ad917e29e76d1999a2a31a8e3", "shasum": "" }, "require": { @@ -11020,7 +11569,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.2.7" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.2.10" }, "funding": [ { @@ -11036,7 +11585,7 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:57:23+00:00" + "time": "2023-04-18T13:46:08+00:00" }, { "name": "symfony/polyfill-php81", @@ -11119,16 +11668,16 @@ }, { "name": "symfony/web-profiler-bundle", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "0d183e0a69652e348007e97ffff8d3ded9cc6d2d" + "reference": "24b6f4370f1cd59aacfc5e799c8614b40776e9c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/0d183e0a69652e348007e97ffff8d3ded9cc6d2d", - "reference": "0d183e0a69652e348007e97ffff8d3ded9cc6d2d", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/24b6f4370f1cd59aacfc5e799c8614b40776e9c8", + "reference": "24b6f4370f1cd59aacfc5e799c8614b40776e9c8", "shasum": "" }, "require": { @@ -11177,7 +11726,7 @@ "description": "Provides a development tool that gives detailed information about the execution of any request", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.2.7" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.2.10" }, "funding": [ { @@ -11193,7 +11742,7 @@ "type": "tidelift" } ], - "time": "2023-02-21T16:32:03+00:00" + "time": "2023-04-24T13:41:17+00:00" }, { "name": "theseer/tokenizer", diff --git a/config/bundles.php b/config/bundles.php index b7dc3d70..ee0f6489 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -15,4 +15,5 @@ Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], + Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], ]; diff --git a/config/packages/rate_limiter.yaml b/config/packages/rate_limiter.yaml index 3538f207..f9375376 100644 --- a/config/packages/rate_limiter.yaml +++ b/config/packages/rate_limiter.yaml @@ -2,5 +2,5 @@ framework: rate_limiter: auth: policy: 'fixed_window' - limit: 5 + limit: 6 interval: '100 seconds' diff --git a/config/packages/scheb_2fa.yaml b/config/packages/scheb_2fa.yaml new file mode 100644 index 00000000..6e38cf2d --- /dev/null +++ b/config/packages/scheb_2fa.yaml @@ -0,0 +1,16 @@ +scheb_two_factor: + security_tokens: + - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken + - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken + - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface + + google: + enabled: true + #server_name: server_name # Server name used in QR code + issuer: issuer_placeholder # Issuer name used in QR code + digits: 6 # Number of digits in authentication code + window: 1 # Depends on the version of Spomky-Labs/otphp used: + # Until v10: How many codes before/after the current one would be accepted + # From v11: Acceptable time drift in seconds + template: auth/two_factor/two_factor_form.html.twig # Template used to render the authentication form + form_renderer: App\Controller\Auth\TwoFactor\EnterAuthCodeController diff --git a/config/packages/security.yaml b/config/packages/security.yaml index fcc91cf1..1bed4c6c 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -30,6 +30,12 @@ security: lazy: true login_throttling: max_attempts: 5 + two_factor: + auth_form_path: 2fa_login # The route name you have used in the routes.yaml + check_path: 2fa_login_check # The route name you have used in the routes.yaml + enable_csrf: true + csrf_parameter: _csrf_security_token + csrf_token_id: two_factor # This allows the user to login by submitting a username and password # Reference: https://symfony.com/doc/current/security/form_login_setup.html @@ -74,6 +80,11 @@ security: # additional security lives in the controllers - { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN } - { path: '^/(%app_locales%)/user', roles: ROLE_USER } + # This makes the logout route accessible during two-factor authentication. Allows the user to + # cancel two-factor authentication, if they need to. + - { path: ^/logout, role: PUBLIC_ACCESS } + # This ensures that the form can only be accessed when two-factor authentication is in progress. + - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } when@test: # this configuration simplifies testing URLs protected by the security mechanism diff --git a/config/routes/sched_2fa.yaml b/config/routes/sched_2fa.yaml new file mode 100644 index 00000000..33020f10 --- /dev/null +++ b/config/routes/sched_2fa.yaml @@ -0,0 +1,7 @@ +2fa_login: + path: /{_locale}/2fa + defaults: + _controller: "scheb_two_factor.form_controller::form" + +2fa_login_check: + path: /{_locale}/2fa_check diff --git a/config/services.yaml b/config/services.yaml index 9b1cf9e5..76736d4a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -7,7 +7,7 @@ parameters: locale: 'en' app_locales: 'en|ru|nl|bg' images_directory: '%kernel.project_dir%/public/uploads/images' - app_version: '2.5.0' + app_version: '2.6.0' services: # default configuration for services in *this* file diff --git a/migrations/Version20230501134351.php b/migrations/Version20230501134351.php new file mode 100644 index 00000000..f7d9709b --- /dev/null +++ b/migrations/Version20230501134351.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE users ADD google_authenticator_secret VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE users DROP google_authenticator_secret'); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2f8af21f..890644a1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ + diff --git a/public/build/css/security.64e38d75.css b/public/build/css/security.64e38d75.css new file mode 100644 index 00000000..0485693e --- /dev/null +++ b/public/build/css/security.64e38d75.css @@ -0,0 +1 @@ +.security-buttons a i{background-color:#fff;border-radius:50%;color:#bdc3c7;padding:20px} \ No newline at end of file diff --git a/public/build/entrypoints.json b/public/build/entrypoints.json index 188cfe8e..43745894 100644 --- a/public/build/entrypoints.json +++ b/public/build/entrypoints.json @@ -75,7 +75,7 @@ "js/user": { "js": [ "/build/runtime.9a71ee5d.js", - "/build/js/user.051ef9ab.js" + "/build/js/user.fa1131dc.js" ] }, "js/password": { @@ -84,6 +84,12 @@ "/build/js/password.a4f1763f.js" ] }, + "js/google_authenticator": { + "js": [ + "/build/runtime.9a71ee5d.js", + "/build/js/google_authenticator.0889962e.js" + ] + }, "js/bottom-bar": { "js": [ "/build/runtime.9a71ee5d.js", @@ -153,6 +159,14 @@ "css": [ "/build/css/bottom-bar.38e93f4e.css" ] + }, + "css/security": { + "js": [ + "/build/runtime.9a71ee5d.js" + ], + "css": [ + "/build/css/security.64e38d75.css" + ] } } } \ No newline at end of file diff --git a/public/build/js/google_authenticator.0889962e.js b/public/build/js/google_authenticator.0889962e.js new file mode 100644 index 00000000..10964ff0 --- /dev/null +++ b/public/build/js/google_authenticator.0889962e.js @@ -0,0 +1 @@ +(self.webpackChunk=self.webpackChunk||[]).push([[931],{861:()=>{!function(e){"use strict";const t=e("#generate_google_auth_secret"),a=e('[name="auth_token"]').val(),n=e('[name="generatedSecret"]'),o=e('[name="authentication_code"]');e("#setUpAuthenticatorButton").click((function(){!0===t.data("generate-new-secret")&&e.ajax({method:"GET",url:"/en/user/google_authenticator_code",data:{csrf_token:a}}).done((function(t){const{secret:a,qr_code:o}=t,c=new Image;c.src=o,n.val(a),e("#generatedQrCode").html(c),e("#generatedSecret").html(a)}))})),e("#enable2fa").click((function(){let c=o.val().trim();c?e.ajax({method:"PUT",url:t.attr("action"),data:{csrf_token:a,secret:n.val(),authentication_code:c}}).done((function(){location.reload()})).fail((function(t){var a;a=t.responseJSON.message,e("#twoFactorAuthErrorMessage").text(a).removeClass("d-none")})):o.addClass("is-invalid").focus()})),e("#disable2fa").click((function(){e.ajax({method:"DELETE",url:t.attr("action"),data:{csrf_token:a}}).done((function(){location.reload()})).fail((function(){location.reload()}))})),o.keyup((function(){e(this).removeClass("is-invalid")})),e("#setUpAuthenticator").on("hidden.bs.modal",(function(){o.val("").removeClass("is-invalid"),e("#twoFactorAuthErrorMessage").text("").addClass("d-none")}))}(window.jQuery)}},e=>{var t;t=861,e(e.s=t)}]); \ No newline at end of file diff --git a/public/build/js/user.051ef9ab.js b/public/build/js/user.fa1131dc.js similarity index 56% rename from public/build/js/user.051ef9ab.js rename to public/build/js/user.fa1131dc.js index a73bb80e..cabbed2c 100644 --- a/public/build/js/user.051ef9ab.js +++ b/public/build/js/user.fa1131dc.js @@ -1 +1 @@ -(self.webpackChunk=self.webpackChunk||[]).push([[733],{509:()=>{!function(t,e){"use strict";t('[data-type="delete"]').click((function(a){a.preventDefault();const s=t(this).closest("form"),i=t(this).data("message"),n=t(this).data("confirmation-text"),c=t(this).data("cancellation-text");e.confirm({message:i,buttons:{cancel:{label:c,className:"btn-light"},confirm:{label:n,className:"btn-danger"}},callback:function(t){t&&s.submit()}})}))}($,bootbox)},799:()=>{!function(t){"use strict";t(".btn-outline-secondary").click((function(e){e.preventDefault(),t(this).addClass("disabled");let a=t(this).attr("href"),s=t(this).parent().parent().parent();s.css({opacity:"0.5"}),t.get(a).done((function(){s.fadeOut(),function(){let e=t(".js-counter"),a=e.text();a=Number.parseInt(a),a-=1,e.text(a)}()}))}))}($)},437:()=>{!function(t){"use strict";let e=window.location.href;-1!==e.indexOf("profile")?t(".list-group-item-action:eq(2)").addClass("active"):-1!==e.indexOf("unpublished")?t(".list-group-item-action:eq(1)").addClass("active"):t(".list-group-item-action:eq(0)").addClass("active")}($)},346:(t,e,a)=>{"use strict";a(437),a(799),a(509)}},t=>{var e;e=346,t(t.s=e)}]); \ No newline at end of file +(self.webpackChunk=self.webpackChunk||[]).push([[733],{509:()=>{!function(t,e){"use strict";t('[data-type="delete"]').click((function(a){a.preventDefault();const i=t(this).closest("form"),s=t(this).data("message"),n=t(this).data("confirmation-text"),c=t(this).data("cancellation-text");e.confirm({message:s,buttons:{cancel:{label:c,className:"btn-light"},confirm:{label:n,className:"btn-danger"}},callback:function(t){t&&i.submit()}})}))}($,bootbox)},799:()=>{!function(t){"use strict";t(".btn-outline-secondary").click((function(e){e.preventDefault(),t(this).addClass("disabled");let a=t(this).attr("href"),i=t(this).parent().parent().parent();i.css({opacity:"0.5"}),t.get(a).done((function(){i.fadeOut(),function(){let e=t(".js-counter"),a=e.text();a=Number.parseInt(a),a-=1,e.text(a)}()}))}))}($)},437:()=>{!function(t){"use strict";let e=window.location.href;-1!==e.indexOf("profile")?t(".list-group-item-action:eq(2)").addClass("active"):-1!==e.indexOf("unpublished")?t(".list-group-item-action:eq(1)").addClass("active"):-1!==e.indexOf("security")?t(".list-group-item-action:eq(3)").addClass("active"):t(".list-group-item-action:eq(0)").addClass("active")}(window.jQuery)},346:(t,e,a)=>{"use strict";a(437),a(799),a(509)}},t=>{var e;e=346,t(t.s=e)}]); \ No newline at end of file diff --git a/public/build/manifest.json b/public/build/manifest.json index 1a68e912..1b0a721b 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -11,8 +11,9 @@ "build/js/city.js": "/build/js/city.0d36870f.js", "build/js/photo.js": "/build/js/photo.452a0a9f.js", "build/js/page.js": "/build/js/page.eaed21af.js", - "build/js/user.js": "/build/js/user.051ef9ab.js", + "build/js/user.js": "/build/js/user.fa1131dc.js", "build/js/password.js": "/build/js/password.a4f1763f.js", + "build/js/google_authenticator.js": "/build/js/google_authenticator.0889962e.js", "build/js/bottom-bar.js": "/build/js/bottom-bar.8f444bd6.js", "build/css/app.css": "/build/css/app.66361058.css", "build/css/admin.css": "/build/css/admin.832d2af4.css", @@ -22,6 +23,7 @@ "build/css/photo.css": "/build/css/photo.a213acff.css", "build/css/select2.css": "/build/css/select2.3f146cf8.css", "build/css/bottom-bar.css": "/build/css/bottom-bar.38e93f4e.css", + "build/css/security.css": "/build/css/security.64e38d75.css", "build/runtime.js": "/build/runtime.9a71ee5d.js", "build/images/fa-solid-900.svg": "/build/images/fa-solid-900.7a8b4f13.svg", "build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.9bbb245e.eot", diff --git a/src/Controller/Ajax/Admin/SettingsController.php b/src/Controller/Ajax/Admin/SettingsController.php index 4c2e814f..ea92e287 100644 --- a/src/Controller/Ajax/Admin/SettingsController.php +++ b/src/Controller/Ajax/Admin/SettingsController.php @@ -13,7 +13,7 @@ final class SettingsController extends AbstractController implements AjaxController { - public function __construct(private SettingsService $service) + public function __construct(private readonly SettingsService $service) { } diff --git a/src/Controller/Ajax/User/Security/GoogleAuthenticatorController.php b/src/Controller/Ajax/User/Security/GoogleAuthenticatorController.php new file mode 100644 index 00000000..f30e665e --- /dev/null +++ b/src/Controller/Ajax/User/Security/GoogleAuthenticatorController.php @@ -0,0 +1,71 @@ +getUser(); + + return new JsonResponse($this->service->generateSecret($user)); + } catch (\Throwable $e) { + return new JsonResponse([ + 'message' => $e->getMessage(), + ], 422); + } + } + + #[Route(path: self::ENDPOINT, name: 'set_auth_code', methods: ['PUT'])] + public function setAuthCode(Request $request): JsonResponse + { + try { + /** @var User $user */ + $user = $this->getUser(); + $authenticationCode = $request->get('authentication_code'); + $secret = $request->get('secret'); + + $this->service->setSecret($user, $secret, $authenticationCode); + + return new JsonResponse(); + } catch (\Throwable $exception) { + return new JsonResponse([ + 'message' => $exception->getMessage(), + ], 422); + } + } + + #[Route(path: self::ENDPOINT, name: 'delete_auth_code', methods: ['DELETE'])] + public function deleteAuthCode(): JsonResponse + { + try { + /** @var User $user */ + $user = $this->getUser(); + $this->service->deleteSecret($user); + } catch (\Throwable) { + $this->addFlash('danger', '2fa.errors.cannot_disable_ga'); + } + + return new JsonResponse(); + } +} diff --git a/src/Controller/Ajax/User/PasswordController.php b/src/Controller/Ajax/User/Security/PasswordController.php similarity index 94% rename from src/Controller/Ajax/User/PasswordController.php rename to src/Controller/Ajax/User/Security/PasswordController.php index 28fd7acb..3b5cc410 100644 --- a/src/Controller/Ajax/User/PasswordController.php +++ b/src/Controller/Ajax/User/Security/PasswordController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Controller\Ajax\User; +namespace App\Controller\Ajax\User\Security; use App\Controller\Ajax\AjaxController; use App\Service\User\PasswordService; diff --git a/src/Controller/Auth/LoginController.php b/src/Controller/Auth/LoginController.php index 45711d01..6f582e8f 100644 --- a/src/Controller/Auth/LoginController.php +++ b/src/Controller/Auth/LoginController.php @@ -38,7 +38,7 @@ public function login(Request $request, Security $security, AuthenticationUtils * @throws \Exception */ #[Route(path: '/logout', name: 'security_logout')] - public function logout(): void + public function logout(): never { throw new \Exception('This should never be reached!'); } diff --git a/src/Controller/Auth/RegisterController.php b/src/Controller/Auth/RegisterController.php index 8e93f67f..7b3bd20d 100644 --- a/src/Controller/Auth/RegisterController.php +++ b/src/Controller/Auth/RegisterController.php @@ -23,14 +23,14 @@ final class RegisterController extends BaseController implements AuthController { - private array $settings; + private readonly array $settings; public function __construct( - private MessageBusInterface $messageBus, - private RegistrationFormAuthenticator $authenticator, - private Security $security, - private UserAuthenticatorInterface $userAuthenticator, - private UserService $service, + private readonly MessageBusInterface $messageBus, + private readonly RegistrationFormAuthenticator $authenticator, + private readonly Security $security, + private readonly UserAuthenticatorInterface $userAuthenticator, + private readonly UserService $service, ManagerRegistry $doctrine, RequestStack $requestStack, SettingsRepository $settingsRepository diff --git a/src/Controller/Auth/ResetPasswordController.php b/src/Controller/Auth/ResetPasswordController.php index 5518eab8..3afc7a6a 100644 --- a/src/Controller/Auth/ResetPasswordController.php +++ b/src/Controller/Auth/ResetPasswordController.php @@ -17,7 +17,7 @@ final class ResetPasswordController extends BaseController implements AuthController { - #[Route(path: '/password/reset', methods: ['GET|POST'], name: 'password_reset')] + #[Route(path: '/password/reset', name: 'password_reset', methods: ['GET|POST'])] public function passwordReset(ResettingService $service, Request $request): Response { $form = $this->createForm(UserEmailType::class, []); @@ -34,7 +34,7 @@ public function passwordReset(ResettingService $service, Request $request): Resp ]); } - #[Route(path: '/password/reset/{token}', methods: ['GET|POST'], name: 'password_reset_confirm')] + #[Route(path: '/password/reset/{token}', name: 'password_reset_confirm', methods: ['GET|POST'])] public function passwordResetConfirm(ResettingRepository $repository, Request $request, string $token): Response { /** @var User $user */ diff --git a/src/Controller/Auth/TwoFactor/EnterAuthCodeController.php b/src/Controller/Auth/TwoFactor/EnterAuthCodeController.php new file mode 100644 index 00000000..136b423f --- /dev/null +++ b/src/Controller/Auth/TwoFactor/EnterAuthCodeController.php @@ -0,0 +1,55 @@ +settingsRepository->findAllAsArray(); + + $this->setDoctrine($this->doctrine); + + $menu = $this->menu($request); + + return array_merge($settings, $menu); + } + + /** + * @throws SyntaxError + * @throws RuntimeError + * @throws LoaderError + */ + public function renderForm(Request $request, array $templateVars): Response + { + $content = $this->twigEnvironment->render('auth/two_factor/two_factor_form.html.twig', [ + 'robots' => 'noindex', + 'site' => $this->site($request), + ]); + + return (new Response()) + ->setContent($content); + } +} diff --git a/src/Controller/Auth/VerificationController.php b/src/Controller/Auth/VerificationController.php index e434c182..09cae52d 100644 --- a/src/Controller/Auth/VerificationController.php +++ b/src/Controller/Auth/VerificationController.php @@ -14,7 +14,7 @@ final class VerificationController extends AbstractController implements AuthController { - public function __construct(private EmailVerifier $emailVerifier) + public function __construct(private readonly EmailVerifier $emailVerifier) { } diff --git a/src/Controller/BaseController.php b/src/Controller/BaseController.php index 835c47e2..30c725dc 100644 --- a/src/Controller/BaseController.php +++ b/src/Controller/BaseController.php @@ -4,11 +4,11 @@ namespace App\Controller; +use App\Controller\Traits\MenuTrait; use App\Entity\Category; use App\Entity\City; use App\Entity\DealType; use App\Entity\Feature; -use App\Entity\Menu; use App\Repository\SettingsRepository; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -16,18 +16,12 @@ abstract class BaseController extends AbstractController { - public function __construct(private SettingsRepository $settingsRepository, protected ManagerRegistry $doctrine) - { - } + use MenuTrait; - private function menu(Request $request): array - { - return [ - 'menu' => $this->doctrine->getRepository(Menu::class) - ->findBy([ - 'locale' => $request->getLocale(), - ], ['sort_order' => 'ASC']), - ]; + public function __construct( + private readonly SettingsRepository $settingsRepository, + protected ManagerRegistry $doctrine + ) { } private function searchFields(): array @@ -62,6 +56,8 @@ public function site(Request $request): array $fields = $this->searchFields(); + $this->setDoctrine($this->doctrine); + $menu = $this->menu($request); return array_merge($settings, $fields, $menu); diff --git a/src/Controller/PageController.php b/src/Controller/PageController.php index ca4111b8..98cab306 100644 --- a/src/Controller/PageController.php +++ b/src/Controller/PageController.php @@ -44,7 +44,7 @@ public function pageShow( [ 'site' => $this->site($request), 'page' => $page, - 'form' => (!empty($form) ? $form : []), + 'form' => (empty($form) ? [] : $form), ] ); } diff --git a/src/Controller/Traits/MenuTrait.php b/src/Controller/Traits/MenuTrait.php new file mode 100644 index 00000000..0fb13c83 --- /dev/null +++ b/src/Controller/Traits/MenuTrait.php @@ -0,0 +1,29 @@ +doctrine = $doctrine; + } + + protected function menu(Request $request): array + { + return [ + 'menu' => $this->doctrine->getRepository(Menu::class) + ->findBy([ + 'locale' => $request->getLocale(), + ], ['sort_order' => 'ASC']), + ]; + } +} diff --git a/src/Controller/User/SecurityController.php b/src/Controller/User/SecurityController.php new file mode 100644 index 00000000..90feec87 --- /dev/null +++ b/src/Controller/User/SecurityController.php @@ -0,0 +1,21 @@ +render('user/security/security.html.twig', [ + 'site' => $this->site($request), + ]); + } +} diff --git a/src/Entity/Category.php b/src/Entity/Category.php index ddb023d9..8df9219b 100644 --- a/src/Entity/Category.php +++ b/src/Entity/Category.php @@ -7,10 +7,11 @@ use App\Entity\Traits\EntityIdTrait; use App\Entity\Traits\EntityNameTrait; use App\Entity\Traits\PropertyTrait; +use App\Repository\CategoryRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -#[ORM\Entity(repositoryClass: 'App\Repository\CategoryRepository')] +#[ORM\Entity(repositoryClass: CategoryRepository::class)] #[UniqueEntity('slug')] class Category { @@ -18,5 +19,5 @@ class Category use EntityNameTrait; use PropertyTrait; - public const MAPPED_BY = 'category'; + final public const MAPPED_BY = 'category'; } diff --git a/src/Entity/City.php b/src/Entity/City.php index b6cef3fe..b554d518 100644 --- a/src/Entity/City.php +++ b/src/Entity/City.php @@ -7,13 +7,14 @@ use App\Entity\Traits\EntityIdTrait; use App\Entity\Traits\EntityMetaTrait; use App\Entity\Traits\EntityNameTrait; +use App\Repository\CityRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -#[ORM\Entity(repositoryClass: 'App\Repository\CityRepository')] +#[ORM\Entity(repositoryClass: CityRepository::class)] #[UniqueEntity('slug')] class City { @@ -21,18 +22,18 @@ class City use EntityMetaTrait; use EntityNameTrait; - #[ORM\OneToMany(mappedBy: 'city', targetEntity: 'App\Entity\Property')] + #[ORM\OneToMany(mappedBy: 'city', targetEntity: Property::class)] private $properties; - #[ORM\OneToMany(mappedBy: 'city', targetEntity: 'App\Entity\District')] + #[ORM\OneToMany(mappedBy: 'city', targetEntity: District::class)] #[ORM\OrderBy(['name' => 'ASC'])] private $districts; - #[ORM\OneToMany(mappedBy: 'city', targetEntity: 'App\Entity\Neighborhood')] + #[ORM\OneToMany(mappedBy: 'city', targetEntity: Neighborhood::class)] #[ORM\OrderBy(['name' => 'ASC'])] private $neighborhoods; - #[ORM\OneToMany(mappedBy: 'city', targetEntity: 'App\Entity\Metro', orphanRemoval: true)] + #[ORM\OneToMany(mappedBy: 'city', targetEntity: Metro::class, orphanRemoval: true)] #[ORM\OrderBy(['name' => 'ASC'])] private $metro_stations; diff --git a/src/Entity/Currency.php b/src/Entity/Currency.php index a48244db..be2d1278 100644 --- a/src/Entity/Currency.php +++ b/src/Entity/Currency.php @@ -5,10 +5,11 @@ namespace App\Entity; use App\Entity\Traits\EntityIdTrait; +use App\Repository\CurrencyRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: 'App\Repository\CurrencyRepository')] +#[ORM\Entity(repositoryClass: CurrencyRepository::class)] class Currency { use EntityIdTrait; diff --git a/src/Entity/DealType.php b/src/Entity/DealType.php index 50d3925c..4eb56822 100644 --- a/src/Entity/DealType.php +++ b/src/Entity/DealType.php @@ -7,10 +7,11 @@ use App\Entity\Traits\EntityIdTrait; use App\Entity\Traits\EntityNameTrait; use App\Entity\Traits\PropertyTrait; +use App\Repository\DealTypeRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -#[ORM\Entity(repositoryClass: 'App\Repository\DealTypeRepository')] +#[ORM\Entity(repositoryClass: DealTypeRepository::class)] #[UniqueEntity('slug')] class DealType { @@ -18,5 +19,5 @@ class DealType use EntityNameTrait; use PropertyTrait; - public const MAPPED_BY = 'deal_type'; + final public const MAPPED_BY = 'deal_type'; } diff --git a/src/Entity/District.php b/src/Entity/District.php index fd7b8c68..4fe71931 100644 --- a/src/Entity/District.php +++ b/src/Entity/District.php @@ -8,10 +8,11 @@ use App\Entity\Traits\EntityIdTrait; use App\Entity\Traits\EntityNameTrait; use App\Entity\Traits\PropertyTrait; +use App\Repository\DistrictRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -#[ORM\Entity(repositoryClass: 'App\Repository\DistrictRepository')] +#[ORM\Entity(repositoryClass: DistrictRepository::class)] #[UniqueEntity('slug')] class District { @@ -20,8 +21,8 @@ class District use EntityNameTrait; use PropertyTrait; - public const MAPPED_BY = 'district'; - public const INVERSED_BY = 'districts'; - public const GETTER = 'getDistrict'; - public const SETTER = 'setDistrict'; + final public const MAPPED_BY = 'district'; + final public const INVERSED_BY = 'districts'; + final public const GETTER = 'getDistrict'; + final public const SETTER = 'setDistrict'; } diff --git a/src/Entity/Feature.php b/src/Entity/Feature.php index 6b95f320..ed19b9f3 100644 --- a/src/Entity/Feature.php +++ b/src/Entity/Feature.php @@ -5,12 +5,13 @@ namespace App\Entity; use App\Entity\Traits\EntityIdTrait; +use App\Repository\FeatureRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: 'App\Repository\FeatureRepository')] +#[ORM\Entity(repositoryClass: FeatureRepository::class)] class Feature { use EntityIdTrait; @@ -18,7 +19,7 @@ class Feature #[ORM\Column(type: Types::STRING, length: 255)] private ?string $name; - #[ORM\ManyToMany(targetEntity: 'App\Entity\Property', mappedBy: 'features')] + #[ORM\ManyToMany(targetEntity: Property::class, mappedBy: 'features')] private $properties; #[ORM\Column(type: Types::TEXT, nullable: true)] diff --git a/src/Entity/Menu.php b/src/Entity/Menu.php index 266a49c4..7b4eadba 100644 --- a/src/Entity/Menu.php +++ b/src/Entity/Menu.php @@ -5,13 +5,14 @@ namespace App\Entity; use App\Entity\Traits\EntityIdTrait; +use App\Repository\MenuRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; #[ORM\Table] #[ORM\UniqueConstraint(name: 'url_locale_unique_key', columns: ['url', 'locale'])] -#[ORM\Entity(repositoryClass: 'App\Repository\MenuRepository')] +#[ORM\Entity(repositoryClass: MenuRepository::class)] #[UniqueEntity(['url', 'locale'])] class Menu { diff --git a/src/Entity/Metro.php b/src/Entity/Metro.php index c840bf8b..3bf761bb 100644 --- a/src/Entity/Metro.php +++ b/src/Entity/Metro.php @@ -8,10 +8,11 @@ use App\Entity\Traits\EntityIdTrait; use App\Entity\Traits\EntityNameTrait; use App\Entity\Traits\PropertyTrait; +use App\Repository\MetroRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -#[ORM\Entity(repositoryClass: 'App\Repository\MetroRepository')] +#[ORM\Entity(repositoryClass: MetroRepository::class)] #[UniqueEntity('slug')] class Metro { @@ -20,8 +21,8 @@ class Metro use EntityNameTrait; use PropertyTrait; - public const MAPPED_BY = 'metro_station'; - public const INVERSED_BY = 'metro_stations'; - public const GETTER = 'getMetroStation'; - public const SETTER = 'setMetroStation'; + final public const MAPPED_BY = 'metro_station'; + final public const INVERSED_BY = 'metro_stations'; + final public const GETTER = 'getMetroStation'; + final public const SETTER = 'setMetroStation'; } diff --git a/src/Entity/Neighborhood.php b/src/Entity/Neighborhood.php index 22f14c92..ab4e755a 100644 --- a/src/Entity/Neighborhood.php +++ b/src/Entity/Neighborhood.php @@ -8,10 +8,11 @@ use App\Entity\Traits\EntityIdTrait; use App\Entity\Traits\EntityNameTrait; use App\Entity\Traits\PropertyTrait; +use App\Repository\NeighborhoodRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -#[ORM\Entity(repositoryClass: 'App\Repository\NeighborhoodRepository')] +#[ORM\Entity(repositoryClass: NeighborhoodRepository::class)] #[UniqueEntity('slug')] class Neighborhood { @@ -20,8 +21,8 @@ class Neighborhood use EntityNameTrait; use PropertyTrait; - public const MAPPED_BY = 'neighborhood'; - public const INVERSED_BY = 'neighborhoods'; - public const GETTER = 'getNeighborhood'; - public const SETTER = 'setNeighborhood'; + final public const MAPPED_BY = 'neighborhood'; + final public const INVERSED_BY = 'neighborhoods'; + final public const GETTER = 'getNeighborhood'; + final public const SETTER = 'setNeighborhood'; } diff --git a/src/Entity/Page.php b/src/Entity/Page.php index e583cfa1..7226a23c 100644 --- a/src/Entity/Page.php +++ b/src/Entity/Page.php @@ -5,6 +5,7 @@ namespace App\Entity; use App\Entity\Traits\EntityIdTrait; +use App\Repository\PageRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -12,7 +13,7 @@ #[ORM\Table] #[ORM\UniqueConstraint(name: 'slug_locale_unique_key', columns: ['slug', 'locale'])] -#[ORM\Entity(repositoryClass: 'App\Repository\PageRepository')] +#[ORM\Entity(repositoryClass: PageRepository::class)] #[UniqueEntity(['slug', 'locale'])] class Page { diff --git a/src/Entity/Photo.php b/src/Entity/Photo.php index d799ca88..32f1c778 100644 --- a/src/Entity/Photo.php +++ b/src/Entity/Photo.php @@ -5,16 +5,17 @@ namespace App\Entity; use App\Entity\Traits\EntityIdTrait; +use App\Repository\PhotoRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; -#[ORM\Entity(repositoryClass: 'App\Repository\PhotoRepository')] +#[ORM\Entity(repositoryClass: PhotoRepository::class)] class Photo { use EntityIdTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Property', inversedBy: 'photos')] + #[ORM\ManyToOne(targetEntity: Property::class, inversedBy: 'photos')] #[ORM\JoinColumn(nullable: true)] private $property; diff --git a/src/Entity/Property.php b/src/Entity/Property.php index d200e989..f22f31e6 100644 --- a/src/Entity/Property.php +++ b/src/Entity/Property.php @@ -8,12 +8,13 @@ use App\Entity\Traits\EntityIdTrait; use App\Entity\Traits\EntityLocationTrait; use App\Entity\Traits\EntityTimestampableTrait; +use App\Repository\PropertyRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: 'App\Repository\PropertyRepository')] +#[ORM\Entity(repositoryClass: PropertyRepository::class)] class Property { use CityTrait; @@ -21,17 +22,17 @@ class Property use EntityLocationTrait; use EntityTimestampableTrait; - public const INVERSED_BY = 'properties'; + final public const INVERSED_BY = 'properties'; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User', inversedBy: 'properties')] + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'properties')] #[ORM\JoinColumn(nullable: false)] private $author; - #[ORM\ManyToOne(targetEntity: 'App\Entity\DealType', inversedBy: 'properties')] + #[ORM\ManyToOne(targetEntity: DealType::class, inversedBy: 'properties')] #[ORM\JoinColumn(nullable: false)] private $deal_type; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Category', inversedBy: 'properties')] + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'properties')] #[ORM\JoinColumn(nullable: false)] private $category; @@ -60,13 +61,13 @@ class Property private $available_now; #[ORM\Column(type: Types::STRING, length: 255, options: ['default' => 'pending'])] - private $state = 'published'; + private string $state = 'published'; - #[ORM\OneToMany(mappedBy: 'property', targetEntity: 'App\Entity\Photo', orphanRemoval: true)] + #[ORM\OneToMany(mappedBy: 'property', targetEntity: Photo::class, orphanRemoval: true)] #[ORM\OrderBy(['sort_order' => 'ASC'])] private $photos; - #[ORM\ManyToMany(targetEntity: 'App\Entity\Feature', inversedBy: 'properties')] + #[ORM\ManyToMany(targetEntity: Feature::class, inversedBy: 'properties')] private $features; #[ORM\Column(type: Types::INTEGER)] diff --git a/src/Entity/Settings.php b/src/Entity/Settings.php index e56fda1e..36cb5d99 100644 --- a/src/Entity/Settings.php +++ b/src/Entity/Settings.php @@ -5,11 +5,12 @@ namespace App\Entity; use App\Entity\Traits\EntityIdTrait; +use App\Repository\SettingsRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -#[ORM\Entity(repositoryClass: 'App\Repository\SettingsRepository')] +#[ORM\Entity(repositoryClass: SettingsRepository::class)] #[UniqueEntity('setting_name')] class Settings { diff --git a/src/Entity/Traits/CityTrait.php b/src/Entity/Traits/CityTrait.php index f380622b..75f9104b 100644 --- a/src/Entity/Traits/CityTrait.php +++ b/src/Entity/Traits/CityTrait.php @@ -9,7 +9,7 @@ trait CityTrait { - #[ORM\ManyToOne(targetEntity: 'App\Entity\City', inversedBy: self::INVERSED_BY)] + #[ORM\ManyToOne(targetEntity: City::class, inversedBy: self::INVERSED_BY)] #[ORM\JoinColumn(nullable: false)] private $city; diff --git a/src/Entity/Traits/EntityLocationTrait.php b/src/Entity/Traits/EntityLocationTrait.php index 17e76549..ee5b7870 100644 --- a/src/Entity/Traits/EntityLocationTrait.php +++ b/src/Entity/Traits/EntityLocationTrait.php @@ -12,15 +12,15 @@ trait EntityLocationTrait { - #[ORM\ManyToOne(targetEntity: 'App\Entity\District', inversedBy: 'properties')] + #[ORM\ManyToOne(targetEntity: District::class, inversedBy: 'properties')] #[ORM\JoinColumn(nullable: true)] private ?District $district; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Neighborhood', inversedBy: 'properties')] + #[ORM\ManyToOne(targetEntity: Neighborhood::class, inversedBy: 'properties')] #[ORM\JoinColumn(nullable: true)] private ?Neighborhood $neighborhood; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Metro', inversedBy: 'properties')] + #[ORM\ManyToOne(targetEntity: Metro::class, inversedBy: 'properties')] #[ORM\JoinColumn(nullable: true)] private ?Metro $metro_station; diff --git a/src/Entity/Traits/PropertyTrait.php b/src/Entity/Traits/PropertyTrait.php index 39ae44df..a54a63de 100644 --- a/src/Entity/Traits/PropertyTrait.php +++ b/src/Entity/Traits/PropertyTrait.php @@ -11,7 +11,7 @@ trait PropertyTrait { - #[ORM\OneToMany(mappedBy: self::MAPPED_BY, targetEntity: 'App\Entity\Property')] + #[ORM\OneToMany(mappedBy: self::MAPPED_BY, targetEntity: Property::class)] private Collection $properties; public function __construct() diff --git a/src/Entity/Traits/TwoFactorTrait.php b/src/Entity/Traits/TwoFactorTrait.php new file mode 100644 index 00000000..8ef17ec6 --- /dev/null +++ b/src/Entity/Traits/TwoFactorTrait.php @@ -0,0 +1,34 @@ +googleAuthenticatorSecret; + } + + public function getGoogleAuthenticatorUsername(): string + { + return $this->username; + } + + public function getGoogleAuthenticatorSecret(): ?string + { + return $this->googleAuthenticatorSecret; + } + + public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void + { + $this->googleAuthenticatorSecret = $googleAuthenticatorSecret; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index f7cbabd4..6ace312f 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -5,29 +5,34 @@ namespace App\Entity; use App\Entity\Traits\EntityIdTrait; +use App\Entity\Traits\TwoFactorTrait; +use App\Repository\UserRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Table(name: 'users')] -#[ORM\Entity(repositoryClass: 'App\Repository\UserRepository')] +#[ORM\Entity(repositoryClass: UserRepository::class)] #[UniqueEntity('email')] -class User implements UserInterface, PasswordAuthenticatedUserInterface +class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface { use EntityIdTrait; + use TwoFactorTrait; /** * Requests older than this many seconds will be considered expired. */ - public const RETRY_TTL = 3600; + final public const RETRY_TTL = 3600; /** * Maximum time that the confirmation token will be valid. */ - public const TOKEN_TTL = 43200; + final public const TOKEN_TTL = 43200; + /** * @var string */ @@ -50,7 +55,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(type: Types::JSON)] private array $roles = []; - #[ORM\OneToMany(mappedBy: 'author', targetEntity: 'App\Entity\Property')] + #[ORM\OneToMany(mappedBy: 'author', targetEntity: Property::class)] private $properties; #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] diff --git a/src/EventSubscriber/ControllerSubscriber.php b/src/EventSubscriber/ControllerSubscriber.php index 1702d208..c1ba9502 100644 --- a/src/EventSubscriber/ControllerSubscriber.php +++ b/src/EventSubscriber/ControllerSubscriber.php @@ -8,14 +8,17 @@ use App\Controller\Auth\AuthController; use App\Middleware\ThrottleRequests; use App\Middleware\VerifyCsrfToken; +use Scheb\TwoFactorBundle\Controller\FormController; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\KernelEvents; final class ControllerSubscriber implements EventSubscriberInterface { - public function __construct(private VerifyCsrfToken $verifyCsrfToken, private ThrottleRequests $throttleRequests) - { + public function __construct( + private readonly VerifyCsrfToken $verifyCsrfToken, + private readonly ThrottleRequests $throttleRequests + ) { } public function onKernelController(ControllerEvent $event): void @@ -27,7 +30,7 @@ public function onKernelController(ControllerEvent $event): void if ($controller instanceof AjaxController) { $this->verifyCsrfToken->handle($event->getRequest()); - } elseif ($controller instanceof AuthController) { + } elseif ($controller instanceof AuthController || $controller instanceof FormController) { $this->throttleRequests->handle($event->getRequest()); } } diff --git a/src/MessageHandler/DeletePhotosHandler.php b/src/MessageHandler/DeletePhotosHandler.php index ec06bed4..c8dcbbd1 100644 --- a/src/MessageHandler/DeletePhotosHandler.php +++ b/src/MessageHandler/DeletePhotosHandler.php @@ -11,7 +11,7 @@ #[AsMessageHandler] final class DeletePhotosHandler { - public function __construct(private FileUploader $fileUploader) + public function __construct(private readonly FileUploader $fileUploader) { } diff --git a/src/MessageHandler/SendEmailConfirmationLinkHandler.php b/src/MessageHandler/SendEmailConfirmationLinkHandler.php index 784997a9..a1759c21 100644 --- a/src/MessageHandler/SendEmailConfirmationLinkHandler.php +++ b/src/MessageHandler/SendEmailConfirmationLinkHandler.php @@ -21,15 +21,12 @@ final class SendEmailConfirmationLinkHandler { use UserDataCache; - private VerifyEmailHelperInterface $verifyEmailHelper; - public function __construct( - VerifyEmailHelperInterface $helper, - private Mailer $mailer, - private UrlGeneratorInterface $router, - private TranslatorInterface $translator + private readonly VerifyEmailHelperInterface $verifyEmailHelper, + private readonly Mailer $mailer, + private readonly UrlGeneratorInterface $router, + private readonly TranslatorInterface $translator ) { - $this->verifyEmailHelper = $helper; } public function __invoke(SendEmailConfirmationLink $sendEmailConfirmationLink): void diff --git a/src/MessageHandler/SendFeedbackHandler.php b/src/MessageHandler/SendFeedbackHandler.php index 958eb328..9499790f 100644 --- a/src/MessageHandler/SendFeedbackHandler.php +++ b/src/MessageHandler/SendFeedbackHandler.php @@ -15,7 +15,7 @@ #[AsMessageHandler] final class SendFeedbackHandler { - public function __construct(private Mailer $mailer, private TranslatorInterface $translator) + public function __construct(private readonly Mailer $mailer, private readonly TranslatorInterface $translator) { } diff --git a/src/MessageHandler/SendResetPasswordLinkHandler.php b/src/MessageHandler/SendResetPasswordLinkHandler.php index 6bec1a07..2388d29b 100644 --- a/src/MessageHandler/SendResetPasswordLinkHandler.php +++ b/src/MessageHandler/SendResetPasswordLinkHandler.php @@ -8,6 +8,7 @@ use App\Mailer\Mailer; use App\Message\SendResetPasswordLink; use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Mime\Address; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -16,16 +17,20 @@ #[AsMessageHandler] final class SendResetPasswordLinkHandler { - public function __construct(private Mailer $mailer, private TranslatorInterface $translator, private UrlGeneratorInterface $router) - { + public function __construct( + private readonly Mailer $mailer, + private readonly TranslatorInterface $translator, + private readonly UrlGeneratorInterface $router + ) { } + /** + * @throws TransportExceptionInterface + */ public function __invoke(SendResetPasswordLink $sendResetPasswordLink): void { - /** @var User $user */ $user = $sendResetPasswordLink->getUser(); - /** @var TemplatedEmail $email */ $email = $this->buildEmail($user); $this->mailer->send($email); diff --git a/src/Repository/CategoryRepository.php b/src/Repository/CategoryRepository.php index 49f49cf1..d55d953c 100644 --- a/src/Repository/CategoryRepository.php +++ b/src/Repository/CategoryRepository.php @@ -6,6 +6,8 @@ use App\Entity\Category; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -21,6 +23,10 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Category::class); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function countAll(): int { $count = $this->createQueryBuilder('c') diff --git a/src/Repository/CityRepository.php b/src/Repository/CityRepository.php index 8b64f118..c9bc34e3 100644 --- a/src/Repository/CityRepository.php +++ b/src/Repository/CityRepository.php @@ -6,6 +6,8 @@ use App\Entity\City; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -21,6 +23,10 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, City::class); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function countAll(): int { $count = $this->createQueryBuilder('l') diff --git a/src/Repository/DealTypeRepository.php b/src/Repository/DealTypeRepository.php index 52ed0edf..f80162e5 100644 --- a/src/Repository/DealTypeRepository.php +++ b/src/Repository/DealTypeRepository.php @@ -6,6 +6,8 @@ use App\Entity\DealType; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -21,6 +23,10 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, DealType::class); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function countAll(): int { $count = $this->createQueryBuilder('o') diff --git a/src/Repository/MenuRepository.php b/src/Repository/MenuRepository.php index 5195cc5c..f5deded8 100644 --- a/src/Repository/MenuRepository.php +++ b/src/Repository/MenuRepository.php @@ -32,7 +32,7 @@ public function reorderItems(array $items): void foreach ($items as $item) { $this->createQueryBuilder('i') - ->update('App\Entity\Menu', 'm') + ->update(Menu::class, 'm') ->set('m.sort_order', $i) ->where('m.id = ?1') ->setParameter(1, $item) diff --git a/src/Repository/PageRepository.php b/src/Repository/PageRepository.php index 3d40d3a3..d1808064 100644 --- a/src/Repository/PageRepository.php +++ b/src/Repository/PageRepository.php @@ -7,6 +7,8 @@ use App\Entity\Page; use App\Entity\Settings; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; use Knp\Component\Pager\Pagination\PaginationInterface; @@ -21,17 +23,15 @@ */ final class PageRepository extends ServiceEntityRepository { - /** - * @var PaginatorInterface - */ - private $paginator; - - public function __construct(ManagerRegistry $registry, PaginatorInterface $paginator) + public function __construct(ManagerRegistry $registry, private readonly PaginatorInterface $paginator) { parent::__construct($registry, Page::class); - $this->paginator = $paginator; } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function countAll(): int { $count = $this->createQueryBuilder('p') diff --git a/src/Repository/PhotoRepository.php b/src/Repository/PhotoRepository.php index ec7cdadc..51f39a86 100644 --- a/src/Repository/PhotoRepository.php +++ b/src/Repository/PhotoRepository.php @@ -28,7 +28,7 @@ public function reorderPhotos(Property $property, array $ids): void foreach ($ids as $id) { $this->createQueryBuilder('i') - ->update('App\Entity\Photo', 'p') + ->update(Photo::class, 'p') ->set('p.sort_order', $i) ->where('p.id = ?1') ->andWhere('p.property = ?2') diff --git a/src/Repository/PropertyRepository.php b/src/Repository/PropertyRepository.php index ce36383e..fb95a6b4 100644 --- a/src/Repository/PropertyRepository.php +++ b/src/Repository/PropertyRepository.php @@ -25,7 +25,7 @@ class PropertyRepository extends ServiceEntityRepository { public function __construct( ManagerRegistry $registry, - private PaginatorInterface $paginator, + private readonly PaginatorInterface $paginator, protected Security $security) { parent::__construct($registry, Property::class); diff --git a/src/Repository/ResettingRepository.php b/src/Repository/ResettingRepository.php index 636325ff..b2fdb7d0 100644 --- a/src/Repository/ResettingRepository.php +++ b/src/Repository/ResettingRepository.php @@ -10,12 +10,9 @@ final class ResettingRepository extends UserRepository { - private $transformer; - - public function __construct(ManagerRegistry $registry, UserTransformer $transformer) + public function __construct(ManagerRegistry $registry, private readonly UserTransformer $transformer) { parent::__construct($registry); - $this->transformer = $transformer; } public function setPassword(User $user, string $plainPassword): void diff --git a/src/Repository/SettingsRepository.php b/src/Repository/SettingsRepository.php index 12860fc4..d812678d 100644 --- a/src/Repository/SettingsRepository.php +++ b/src/Repository/SettingsRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; +use App\Entity\Currency; use App\Entity\Settings; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -16,15 +17,9 @@ */ final class SettingsRepository extends ServiceEntityRepository { - /** - * @var CurrencyRepository - */ - private $currency; - - public function __construct(ManagerRegistry $registry, CurrencyRepository $currency) + public function __construct(ManagerRegistry $registry, private readonly CurrencyRepository $currency) { parent::__construct($registry, Settings::class); - $this->currency = $currency; } public function findAllAsArray(): array @@ -37,7 +32,7 @@ public function findAllAsArray(): array $settingsArray[$setting->getSettingName()] = $setting->getSettingValue(); } else { $currency = $this->currency->find((int) $setting->getSettingValue()); - if (!$currency) { + if (!$currency instanceof Currency) { $currency = $this->currency->findOneBy(['code' => 'USD']); } $settingsArray['currency'] = $currency; diff --git a/src/Repository/SimilarRepository.php b/src/Repository/SimilarRepository.php index 45371664..a5f85da3 100644 --- a/src/Repository/SimilarRepository.php +++ b/src/Repository/SimilarRepository.php @@ -17,7 +17,7 @@ public function findSimilarProperties(Property $property): array return []; } - if ($property->getNeighborhood()) { + if (null !== $property->getNeighborhood()) { // Find in a small area $result = $this->findByArea($property, 'neighborhood'); @@ -27,7 +27,7 @@ public function findSimilarProperties(Property $property): array } return $result; - } elseif ($property->getDistrict()) { + } elseif (null !== $property->getDistrict()) { return $this->findByArea($property); } diff --git a/src/Repository/UserPropertyRepository.php b/src/Repository/UserPropertyRepository.php index d9f984f8..c94d9c50 100644 --- a/src/Repository/UserPropertyRepository.php +++ b/src/Repository/UserPropertyRepository.php @@ -35,7 +35,7 @@ public function changeState(Property $property, string $state): bool $em->flush(); return true; - } catch (\Throwable $e) { + } catch (\Throwable) { return false; } } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 0ea63e83..34c38857 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -6,6 +6,8 @@ use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -21,6 +23,10 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, User::class); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function countAll(): int { $count = $this->createQueryBuilder('u') diff --git a/src/Service/AbstractService.php b/src/Service/AbstractService.php index 265969c9..7935035b 100644 --- a/src/Service/AbstractService.php +++ b/src/Service/AbstractService.php @@ -14,12 +14,10 @@ abstract class AbstractService { use ClearCache; - private SessionInterface $session; - private CsrfTokenManagerInterface $tokenManager; + private readonly SessionInterface $session; - public function __construct(CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack) + public function __construct(private readonly CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack) { - $this->tokenManager = $tokenManager; $this->session = $requestStack->getSession(); } diff --git a/src/Service/Admin/CategoryService.php b/src/Service/Admin/CategoryService.php index df0e01d7..d8631b0f 100644 --- a/src/Service/Admin/CategoryService.php +++ b/src/Service/Admin/CategoryService.php @@ -12,15 +12,12 @@ final class CategoryService extends AbstractService { - private EntityManagerInterface $em; - public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - EntityManagerInterface $entityManager + private readonly EntityManagerInterface $em ) { parent::__construct($tokenManager, $requestStack); - $this->em = $entityManager; } public function create(Category $category): void diff --git a/src/Service/Admin/CityService.php b/src/Service/Admin/CityService.php index 1655ac0c..97bb7712 100644 --- a/src/Service/Admin/CityService.php +++ b/src/Service/Admin/CityService.php @@ -12,15 +12,12 @@ final class CityService extends AbstractService { - private EntityManagerInterface $em; - public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - EntityManagerInterface $entityManager + private readonly EntityManagerInterface $em ) { parent::__construct($tokenManager, $requestStack); - $this->em = $entityManager; } public function create(City $city): void diff --git a/src/Service/Admin/DealTypeService.php b/src/Service/Admin/DealTypeService.php index a48a14f7..6c3e6b0e 100644 --- a/src/Service/Admin/DealTypeService.php +++ b/src/Service/Admin/DealTypeService.php @@ -12,15 +12,12 @@ final class DealTypeService extends AbstractService { - private EntityManagerInterface $em; - public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - EntityManagerInterface $entityManager + private readonly EntityManagerInterface $em ) { parent::__construct($tokenManager, $requestStack); - $this->em = $entityManager; } public function create(DealType $dealType): void diff --git a/src/Service/Admin/PageService.php b/src/Service/Admin/PageService.php index 9e3aa280..e3e016e3 100644 --- a/src/Service/Admin/PageService.php +++ b/src/Service/Admin/PageService.php @@ -13,15 +13,12 @@ final class PageService extends AbstractService { - private EntityManagerInterface $em; - public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - EntityManagerInterface $entityManager + private readonly EntityManagerInterface $em ) { parent::__construct($tokenManager, $requestStack); - $this->em = $entityManager; } public function create(Page $page): void @@ -62,7 +59,7 @@ public function delete(Page $page): void // Delete a menu item $menu = $this->em->getRepository(Menu::class)->findOneBy(['url' => '/info/'.($page->getSlug() ?? '')]); - if ($menu) { + if (null !== $menu) { $this->remove($menu); } } diff --git a/src/Service/Admin/PropertyService.php b/src/Service/Admin/PropertyService.php index e91c9b4f..0c3dbd4e 100644 --- a/src/Service/Admin/PropertyService.php +++ b/src/Service/Admin/PropertyService.php @@ -18,9 +18,9 @@ class PropertyService extends AbstractService public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - private EntityManagerInterface $em, - private MessageBusInterface $messageBus, - private Slugger $slugger + private readonly EntityManagerInterface $em, + private readonly MessageBusInterface $messageBus, + private readonly Slugger $slugger ) { parent::__construct($tokenManager, $requestStack); } diff --git a/src/Service/Admin/SettingsService.php b/src/Service/Admin/SettingsService.php index 3a7469bb..74cc1c6b 100644 --- a/src/Service/Admin/SettingsService.php +++ b/src/Service/Admin/SettingsService.php @@ -16,18 +16,13 @@ final class SettingsService extends AbstractService { - private SettingsRepository $repository; - private FileUploader $fileUploader; - public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - SettingsRepository $repository, - FileUploader $fileUploader + private readonly SettingsRepository $repository, + private readonly FileUploader $fileUploader ) { parent::__construct($tokenManager, $requestStack); - $this->repository = $repository; - $this->fileUploader = $fileUploader; } /** diff --git a/src/Service/Admin/UserService.php b/src/Service/Admin/UserService.php index dff9fac1..38780e5d 100644 --- a/src/Service/Admin/UserService.php +++ b/src/Service/Admin/UserService.php @@ -13,18 +13,13 @@ final class UserService extends AbstractService { - private EntityManagerInterface $em; - private UserTransformer $transformer; - public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - EntityManagerInterface $entityManager, - UserTransformer $transformer + private readonly EntityManagerInterface $em, + private readonly UserTransformer $transformer ) { parent::__construct($tokenManager, $requestStack); - $this->em = $entityManager; - $this->transformer = $transformer; } public function create(User $user): void diff --git a/src/Service/Auth/EmailVerifier.php b/src/Service/Auth/EmailVerifier.php index 75eff650..cbe583df 100644 --- a/src/Service/Auth/EmailVerifier.php +++ b/src/Service/Auth/EmailVerifier.php @@ -13,8 +13,8 @@ final class EmailVerifier { public function __construct( - private VerifyEmailHelperInterface $verifyEmailHelper, - private EntityManagerInterface $entityManager) + private readonly VerifyEmailHelperInterface $verifyEmailHelper, + private readonly EntityManagerInterface $entityManager) { } diff --git a/src/Service/Auth/ResettingService.php b/src/Service/Auth/ResettingService.php index c5ed603f..c4203235 100644 --- a/src/Service/Auth/ResettingService.php +++ b/src/Service/Auth/ResettingService.php @@ -16,21 +16,14 @@ final class ResettingService extends AbstractService { - private ResettingRepository $repository; - private MessageBusInterface $messageBus; - private TokenGenerator $generator; - public function __construct( CsrfTokenManagerInterface $tokenManager, RequestStack $requestStack, - ResettingRepository $repository, - MessageBusInterface $messageBus, - TokenGenerator $generator + private readonly ResettingRepository $repository, + private readonly MessageBusInterface $messageBus, + private readonly TokenGenerator $generator ) { parent::__construct($tokenManager, $requestStack); - $this->repository = $repository; - $this->messageBus = $messageBus; - $this->generator = $generator; } public function sendResetPasswordLink(Request $request): void diff --git a/src/Service/CityService.php b/src/Service/CityService.php index ba9e7c20..41ba0ec1 100644 --- a/src/Service/CityService.php +++ b/src/Service/CityService.php @@ -13,8 +13,8 @@ final class CityService { public function __construct( - private RequestToArrayTransformer $transformer, - private FilterRepository $repository + private readonly RequestToArrayTransformer $transformer, + private readonly FilterRepository $repository ) { } diff --git a/src/Service/FileUploader.php b/src/Service/FileUploader.php index f75e820c..2325cf05 100644 --- a/src/Service/FileUploader.php +++ b/src/Service/FileUploader.php @@ -20,12 +20,10 @@ final class FileUploader { - private string $targetDirectory; - private Filesystem $fileSystem; + private readonly Filesystem $fileSystem; - public function __construct(string $targetDirectory) + public function __construct(private readonly string $targetDirectory) { - $this->targetDirectory = $targetDirectory; $this->fileSystem = new Filesystem(); } diff --git a/src/Service/URLService.php b/src/Service/URLService.php index 4d4fec28..822c325d 100644 --- a/src/Service/URLService.php +++ b/src/Service/URLService.php @@ -11,7 +11,7 @@ final class URLService { - public function __construct(private RouterInterface $router) + public function __construct(private readonly RouterInterface $router) { } @@ -41,10 +41,6 @@ public function generateCanonical(Property $property): string // Check referer host. public function isRefererFromCurrentHost(Request $request): bool { - if (preg_match('/'.$request->getHost().'/', $request->server->getHeaders()['REFERER'] ?? '')) { - return true; - } - - return false; + return (bool) preg_match('/'.$request->getHost().'/', $request->server->getHeaders()['REFERER'] ?? ''); } } diff --git a/src/Service/User/GoogleAuthenticatorAdapter.php b/src/Service/User/GoogleAuthenticatorAdapter.php new file mode 100644 index 00000000..ac2aeca9 --- /dev/null +++ b/src/Service/User/GoogleAuthenticatorAdapter.php @@ -0,0 +1,37 @@ +authenticator->checkCode($user, $code); + } + + public function getQRContent(TwoFactorInterface $user): string + { + return str_replace( + 'issuer_placeholder', + ucfirst($this->requestStack->getCurrentRequest()->getHost()), + $this->authenticator->getQRContent($user) + ); + } + + public function generateSecret(): string + { + return $this->authenticator->generateSecret(); + } +} diff --git a/src/Service/User/GoogleAuthenticatorService.php b/src/Service/User/GoogleAuthenticatorService.php new file mode 100644 index 00000000..d9100034 --- /dev/null +++ b/src/Service/User/GoogleAuthenticatorService.php @@ -0,0 +1,95 @@ +isGoogleAuthenticatorEnabled()) { + throw new \LogicException($this->translator->trans('2fa.errors.secret_is_already_set')); + } + + $secret = $this->googleAuthenticator->generateSecret(); + $user->setGoogleAuthenticatorSecret($secret); + $qrContent = $this->googleAuthenticator->getQRContent($user); + + parse_str($qrContent, $queryArray); + + return [ + 'secret' => $queryArray['secret'], + 'qr_code' => $this->getEncodedQrCode($qrContent), + ]; + } + + /** + * @throws \Exception + */ + public function setSecret(TwoFactorInterface $user, ?string $secret, ?string $authenticationCode): void + { + if (!$authenticationCode || !$secret) { + throw new \Exception($this->translator->trans('2fa.errors.cannot_enable_ga')); + } + + $user->setGoogleAuthenticatorSecret($secret); + + if (!$this->googleAuthenticator->checkCode($user, $authenticationCode)) { + throw new \Exception($this->translator->trans('2fa.errors.incorrect_ga_code')); + } + + $this->entityManager->flush(); + $this->addFlash('success', '2fa.messages.enabled'); + } + + public function deleteSecret(TwoFactorInterface $user): void + { + $user->setGoogleAuthenticatorSecret(null); + $this->entityManager->flush(); + $this->addFlash('success', '2fa.messages.disabled'); + } + + private function getEncodedQrCode(string $qrCodeContent): string + { + $result = Builder::create() + ->writer(new PngWriter()) + ->writerOptions([]) + ->data($qrCodeContent) + ->encoding(new Encoding('UTF-8')) + ->errorCorrectionLevel(new ErrorCorrectionLevelHigh()) + ->size(200) + ->margin(0) + ->roundBlockSizeMode(new RoundBlockSizeModeMargin()) + ->build(); + + return 'data:image/png;base64,'.base64_encode($result->getString()); + } +} diff --git a/src/Service/User/PasswordService.php b/src/Service/User/PasswordService.php index 2a748a79..9e95adeb 100644 --- a/src/Service/User/PasswordService.php +++ b/src/Service/User/PasswordService.php @@ -15,9 +15,9 @@ final class PasswordService { public function __construct( - private UserService $service, - private TokenStorageInterface $tokenStorage, - private ValidatorInterface $validator + private readonly UserService $service, + private readonly TokenStorageInterface $tokenStorage, + private readonly ValidatorInterface $validator ) { } diff --git a/src/Service/User/PropertyService.php b/src/Service/User/PropertyService.php index 6a2b67dc..5b4339a3 100644 --- a/src/Service/User/PropertyService.php +++ b/src/Service/User/PropertyService.php @@ -26,10 +26,10 @@ public function __construct( EntityManagerInterface $em, MessageBusInterface $messageBus, Slugger $slugger, - private PropertyTransformer $propertyTransformer, - private UserPropertyRepository $repository, - private RequestToArrayTransformer $transformer, - private TokenStorageInterface $tokenStorage + private readonly PropertyTransformer $propertyTransformer, + private readonly UserPropertyRepository $repository, + private readonly RequestToArrayTransformer $transformer, + private readonly TokenStorageInterface $tokenStorage ) { parent::__construct($tokenManager, $requestStack, $em, $messageBus, $slugger); } diff --git a/src/Validator/RegisteredUserValidator.php b/src/Validator/RegisteredUserValidator.php index 4a7e30a3..20ec0ba0 100644 --- a/src/Validator/RegisteredUserValidator.php +++ b/src/Validator/RegisteredUserValidator.php @@ -4,13 +4,14 @@ namespace App\Validator; +use App\Entity\User; use App\Repository\UserRepository; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; final class RegisteredUserValidator extends ConstraintValidator { - public function __construct(private UserRepository $userRepository) + public function __construct(private readonly UserRepository $userRepository) { } @@ -24,7 +25,7 @@ public function validate($value, Constraint $constraint): void $existingUser = $this->userRepository->findOneBy(['email' => $value]); - if (null === $existingUser) { + if (!$existingUser instanceof User) { $this->context->buildViolation($constraint->message) ->addViolation(); } diff --git a/symfony.lock b/symfony.lock index d0b5266e..f1f68f18 100644 --- a/symfony.lock +++ b/symfony.lock @@ -76,6 +76,19 @@ "tests/bootstrap.php" ] }, + "scheb/2fa-bundle": { + "version": "6.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "1e6f68089146853a790b5da9946fc5974f6fcd49" + }, + "files": [ + "config/packages/scheb_2fa.yaml", + "config/routes/scheb_2fa.yaml" + ] + }, "symfony/console": { "version": "6.2", "recipe": { diff --git a/templates/auth/two_factor/two_factor_form.html.twig b/templates/auth/two_factor/two_factor_form.html.twig new file mode 100644 index 00000000..72fe1349 --- /dev/null +++ b/templates/auth/two_factor/two_factor_form.html.twig @@ -0,0 +1,51 @@ +{% extends 'layout/base.html.twig' %} + +{% block title %}{{ 'title.login' | trans }}{% endblock %} + +{% block body %} + +
+
+
+
+
{{ '2fa.google_authenticator_code'|trans }}
+
+ +
+ + + +
+ + +
+ +
+
+ + + + {{ 'action.cancel'|trans }} + +
+
+ +
+ +
+
+
+
+ +{% endblock %} diff --git a/templates/user/common/_floating_action_button.html.twig b/templates/user/common/_floating_action_button.html.twig new file mode 100644 index 00000000..5e868f6f --- /dev/null +++ b/templates/user/common/_floating_action_button.html.twig @@ -0,0 +1,13 @@ +{% if is_granted('ROLE_ADMIN') %} + + + + + +{% elseif app.user.isVerified %} + + + + + +{% endif %} diff --git a/templates/user/common/_sidebar.html.twig b/templates/user/common/_sidebar.html.twig index 8f7306e0..feba3a25 100644 --- a/templates/user/common/_sidebar.html.twig +++ b/templates/user/common/_sidebar.html.twig @@ -1,23 +1,25 @@ -
-
-

{{ 'title.menu'|trans }}

+
+
+
+

{{ 'title.menu'|trans }}

+
-
- diff --git a/templates/user/photo/edit.html.twig b/templates/user/photo/edit.html.twig index 1b4eddf4..7fca59ea 100644 --- a/templates/user/photo/edit.html.twig +++ b/templates/user/photo/edit.html.twig @@ -84,11 +84,7 @@
-
- - {{ include('user/common/_sidebar.html.twig') }} - {{ include('user/common/_change_password.html.twig') }} -
+ {{ include('user/common/_sidebar.html.twig') }}
{% endblock %} @@ -96,7 +92,6 @@ {% block javascripts %} {{ encore_entry_script_tags('js/photo') }} - {{ encore_entry_script_tags('js/password') }} {{ encore_entry_script_tags('js/user') }} {% endblock %} diff --git a/templates/user/profile/profile.html.twig b/templates/user/profile/profile.html.twig index ccfe9747..59cb1ab2 100644 --- a/templates/user/profile/profile.html.twig +++ b/templates/user/profile/profile.html.twig @@ -35,32 +35,16 @@ -
- - {{ include('user/common/_sidebar.html.twig') }} - {{ include('user/common/_change_password.html.twig') }} -
+ {{ include('user/common/_sidebar.html.twig') }} - {% if is_granted('ROLE_ADMIN') %} - - - - - - {% elseif app.user.isVerified %} - - - - + {{ include('user/common/_floating_action_button.html.twig') }} - {% endif %} {% endblock %} {% block javascripts %} {{ encore_entry_script_tags('js/user') }} - {{ encore_entry_script_tags('js/password') }} {% endblock %} diff --git a/templates/user/property/edit.html.twig b/templates/user/property/edit.html.twig index e19780ed..5b101004 100644 --- a/templates/user/property/edit.html.twig +++ b/templates/user/property/edit.html.twig @@ -141,11 +141,7 @@ -
- - {{ include('user/common/_sidebar.html.twig') }} - {{ include('user/common/_change_password.html.twig') }} -
+ {{ include('user/common/_sidebar.html.twig') }} {% endblock %} @@ -154,7 +150,6 @@ {{ encore_entry_script_tags('js/city') }} {{ encore_entry_script_tags('js/select2') }} - {{ encore_entry_script_tags('js/password') }} {% if isHtmlAllowed == true %} diff --git a/templates/user/property/index.html.twig b/templates/user/property/index.html.twig index 1e35ae50..8edc021f 100644 --- a/templates/user/property/index.html.twig +++ b/templates/user/property/index.html.twig @@ -19,27 +19,11 @@ {{ include('user/property/partials/_properties.html.twig') }} -
- - {{ include('user/common/_sidebar.html.twig') }} - {{ include('user/common/_change_password.html.twig') }} -
+ {{ include('user/common/_sidebar.html.twig') }} - {% if is_granted('ROLE_ADMIN') %} - - - - - - {% elseif app.user.isVerified %} - - - - - - {% endif %} + {{ include('user/common/_floating_action_button.html.twig') }} {{ knp_pagination_render(properties) }} @@ -48,6 +32,5 @@ {% block javascripts %} {{ encore_entry_script_tags('js/user') }} - {{ encore_entry_script_tags('js/password') }} {% endblock %} diff --git a/templates/user/property/new.html.twig b/templates/user/property/new.html.twig index 4b2c9cd2..239c5f8b 100644 --- a/templates/user/property/new.html.twig +++ b/templates/user/property/new.html.twig @@ -141,11 +141,7 @@ -
- - {{ include('user/common/_sidebar.html.twig') }} - {{ include('user/common/_change_password.html.twig') }} -
+ {{ include('user/common/_sidebar.html.twig') }} {% endblock %} @@ -154,7 +150,6 @@ {{ encore_entry_script_tags('js/city') }} {{ encore_entry_script_tags('js/select2') }} - {{ encore_entry_script_tags('js/password') }} {% if isHtmlAllowed == true %} diff --git a/templates/user/common/_change_password.html.twig b/templates/user/security/_change_password.html.twig similarity index 98% rename from templates/user/common/_change_password.html.twig rename to templates/user/security/_change_password.html.twig index 5960eb41..e6314044 100644 --- a/templates/user/common/_change_password.html.twig +++ b/templates/user/security/_change_password.html.twig @@ -52,7 +52,7 @@ + + + + + diff --git a/templates/user/security/security.html.twig b/templates/user/security/security.html.twig new file mode 100644 index 00000000..9b4edfcf --- /dev/null +++ b/templates/user/security/security.html.twig @@ -0,0 +1,67 @@ +{% extends 'layout/base.html.twig' %} + +{% block title %}{{ 'title.security'|trans }}{% endblock %} + + {% block stylesheets %} + {{ encore_entry_link_tags('css/security') }} + {% endblock %} + +{% block body %} + +
+ +
+ +
+
+

{{ 'title.security'|trans }}

+
+
+ + +
+ + {{ include('user/common/_sidebar.html.twig') }} + +
+ + {{ include('user/common/_floating_action_button.html.twig') }} + {{ include('user/security/_change_password.html.twig') }} + {{ include('user/security/_google_authenticator.html.twig') }} + +{% endblock %} + +{% block javascripts %} + + {{ encore_entry_script_tags('js/user') }} + {{ encore_entry_script_tags('js/password') }} + {{ encore_entry_script_tags('js/google_authenticator') }} + +{% endblock %} diff --git a/tests/E2E/User/GoogleAuthenticatorTest.php b/tests/E2E/User/GoogleAuthenticatorTest.php new file mode 100644 index 00000000..80cc86ce --- /dev/null +++ b/tests/E2E/User/GoogleAuthenticatorTest.php @@ -0,0 +1,126 @@ +login($client, 'user', 'user'); + $client->clickLink('Security'); + $client->waitFor('[data-target="#setUpAuthenticator"]'); + + // Open the modal window + $client->clickLink('Set up Google Authenticator'); + $crawler = $client->waitForVisibility('#generatedSecret'); + $secret = $crawler->filter('#generatedSecret')->text(); + $this->assertSame(52, mb_strlen($secret)); + + self::$secret = $secret; + + // Enter wrong one time password + $crawler->filter('#generate_google_auth_secret')->form([ + 'authentication_code' => random_int(100000, 999999), + ]); + $crawler->filter('#enable2fa')->click(); + $client->waitForVisibility('#twoFactorAuthErrorMessage'); + + $this->assertSame( + 'The Google Authenticator code is incorrect or has expired', + $crawler->filter('#twoFactorAuthErrorMessage')->text() + ); + + // Generate correct one time password + $ga = new \PHPGangsta_GoogleAuthenticator(); + $oneTimePassword = $ga->getCode($secret); + + // Enter correct one time password + $crawler->filter('#generate_google_auth_secret')->form([ + 'authentication_code' => $oneTimePassword, + ]); + $crawler->filter('#enable2fa')->click(); + $client->waitForVisibility('.alert-success'); + + // Log Out + $this->logout($client); + $this->assertSelectorTextContains('.h3', 'Popular Listing'); + } + + /** + * @throws NoSuchElementException + * @throws TimeoutException + */ + public function testDisableAuthenticator(): void + { + $client = self::createPantherClient(); + + // Try to log in + $client->clickLink('Log in'); + $crawler = $client->waitForVisibility('[name="login_form"]'); + + // Enter credentials + $crawler->filter('[name="login_form"]')->form([ + 'login_form[username]' => 'user', + 'login_form[password]' => 'user', + ]); + $crawler->filter(self::PRIMARY_BUTTON)->click(); + + // Try wrong one time password + $crawler = $client->waitFor('#_auth_code'); + $crawler->filter('#otp')->form([ + '_auth_code' => random_int(100000, 999999), + ]); + $crawler->filter(self::PRIMARY_BUTTON)->click(); + $this->assertSelectorTextContains('.card-header', 'Google Authenticator code'); + + // Generate correct one time password + $ga = new \PHPGangsta_GoogleAuthenticator(); + $oneTimePassword = $ga->getCode(self::$secret); + + $crawler = $client->waitForVisibility('#otp'); + + // Enter valid one time password + $crawler->filter('#otp')->form([ + '_auth_code' => $oneTimePassword, + ]); + + $crawler->filter(self::PRIMARY_BUTTON)->click(); + $client->waitFor('h3'); + $this->assertSelectorTextContains('h3', 'My properties'); + + // Go to the Security page and disable Google Authenticator + $client->clickLink('Security'); + + // Open the modal window + $client->clickLink('Set up Google Authenticator'); + $crawler = $client->waitForVisibility('#disable2fa'); + + $crawler->filter('#disable2fa')->click(); + $client->waitForVisibility('.alert-success'); + + // Log Out + $this->logout($client); + $this->assertSelectorTextContains('.h3', 'Popular Listing'); + } +} diff --git a/tests/E2E/User/PasswordChangeTest.php b/tests/E2E/User/PasswordChangeTest.php index b1065232..7694803b 100644 --- a/tests/E2E/User/PasswordChangeTest.php +++ b/tests/E2E/User/PasswordChangeTest.php @@ -26,6 +26,7 @@ public function testPasswordChange(): void // Log In as a User $client = self::createPantherClient(); $this->login($client, 'user', 'user'); + $client->clickLink('Security'); $client->waitFor('[data-target="#changePassword"]'); // Open the modal window diff --git a/tests/Functional/Controller/Admin/AbstractLocationControllerTest.php b/tests/Functional/Controller/Admin/AbstractLocationControllerTest.php new file mode 100644 index 00000000..f09a1920 --- /dev/null +++ b/tests/Functional/Controller/Admin/AbstractLocationControllerTest.php @@ -0,0 +1,26 @@ +client = $this->authAsAdmin($this); + } +} diff --git a/tests/Functional/Controller/Admin/CityControllerTest.php b/tests/Functional/Controller/Admin/CityControllerTest.php index 9f0298f9..4046a200 100644 --- a/tests/Functional/Controller/Admin/CityControllerTest.php +++ b/tests/Functional/Controller/Admin/CityControllerTest.php @@ -5,26 +5,16 @@ namespace App\Tests\Functional\Controller\Admin; use App\Entity\City; -use App\Tests\Helper\WebTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; -final class CityControllerTest extends WebTestCase +final class CityControllerTest extends AbstractLocationControllerTest { - use WebTestHelper; - - private const NAME = 'Test'; - private const SLUG = 'test'; - private const EDITED_NAME = 'Edited'; - /** * This test changes the database contents by creating a new City. */ public function testAdminNewCity(): void { - $client = $this->authAsAdmin($this); - - $crawler = $client->request('GET', '/en/admin/locations/city/new'); + $crawler = $this->client->request('GET', '/en/admin/locations/city/new'); $form = $crawler->selectButton('Create city')->form([ 'city[name]' => self::NAME, @@ -33,10 +23,10 @@ public function testAdminNewCity(): void 'city[meta_title]' => 'Custom Meta Title', 'city[meta_description]' => 'Custom Meta Description', ]); - $client->submit($form); + $this->client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); - $city = $this->getRepository($client, City::class) + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); + $city = $this->getRepository($this->client, City::class) ->findOneBy([ 'slug' => self::SLUG, ]); @@ -53,14 +43,12 @@ public function testAdminNewCity(): void */ public function testAdminEditCity(): void { - $client = $this->authAsAdmin($this); - - $city = $this->getRepository($client, City::class) + $city = $this->getRepository($this->client, City::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $crawler = $client->request('GET', '/en/admin/locations/city/'.$city.'/edit'); + $crawler = $this->client->request('GET', '/en/admin/locations/city/'.$city.'/edit'); $form = $crawler->selectButton('Save changes')->form([ 'city[name]' => self::EDITED_NAME, @@ -69,10 +57,10 @@ public function testAdminEditCity(): void 'city[meta_description]' => 'Edited Meta Description', ]); - $client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $this->client->submit($form); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $editedCity = $this->getRepository($client, City::class) + $editedCity = $this->getRepository($this->client, City::class) ->findOneBy([ 'id' => $city, ]); @@ -87,18 +75,16 @@ public function testAdminEditCity(): void */ public function testAdminDeleteCity(): void { - $client = $this->authAsAdmin($this); - - $city = $this->getRepository($client, City::class) + $city = $this->getRepository($this->client, City::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $crawler = $client->request('GET', '/en/admin/locations/city'); - $client->submit($crawler->filter('#delete-form-'.$city)->form()); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $crawler = $this->client->request('GET', '/en/admin/locations/city'); + $this->client->submit($crawler->filter('#delete-form-'.$city)->form()); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $this->assertNull($this->getRepository($client, City::class) + $this->assertNull($this->getRepository($this->client, City::class) ->findOneBy([ 'slug' => self::SLUG, ])); diff --git a/tests/Functional/Controller/Admin/DistrictControllerTest.php b/tests/Functional/Controller/Admin/DistrictControllerTest.php index 5a10d0b0..ddf9d009 100644 --- a/tests/Functional/Controller/Admin/DistrictControllerTest.php +++ b/tests/Functional/Controller/Admin/DistrictControllerTest.php @@ -5,34 +5,25 @@ namespace App\Tests\Functional\Controller\Admin; use App\Entity\District; -use App\Tests\Helper\WebTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; -final class DistrictControllerTest extends WebTestCase +final class DistrictControllerTest extends AbstractLocationControllerTest { - use WebTestHelper; - - private const NAME = 'Test'; - private const SLUG = 'test'; - private const EDITED_NAME = 'Edited'; - /** * This test changes the database contents by creating a new District. */ public function testAdminNewDistrict(): void { - $client = $this->authAsAdmin($this); - $crawler = $client->request('GET', '/en/admin/locations/district/new'); + $crawler = $this->client->request('GET', '/en/admin/locations/district/new'); $form = $crawler->selectButton('Create district')->form([ 'district[name]' => self::NAME, 'district[slug]' => self::SLUG, ]); - $client->submit($form); + $this->client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); - $district = $this->getRepository($client, District::class)->findOneBy([ + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); + $district = $this->getRepository($this->client, District::class)->findOneBy([ 'slug' => self::SLUG, ]); @@ -46,23 +37,21 @@ public function testAdminNewDistrict(): void */ public function testAdminEditDistrict(): void { - $client = $this->authAsAdmin($this); - - $district = $this->getRepository($client, District::class) + $district = $this->getRepository($this->client, District::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $crawler = $client->request('GET', '/en/admin/locations/district/'.$district.'/edit'); + $crawler = $this->client->request('GET', '/en/admin/locations/district/'.$district.'/edit'); $form = $crawler->selectButton('Save changes')->form([ 'district[name]' => self::EDITED_NAME, ]); - $client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $this->client->submit($form); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $editedDistrict = $this->getRepository($client, District::class) + $editedDistrict = $this->getRepository($this->client, District::class) ->findOneBy([ 'id' => $district, ]); @@ -75,19 +64,17 @@ public function testAdminEditDistrict(): void */ public function testAdminDeleteDistrict(): void { - $client = $this->authAsAdmin($this); - - $crawler = $client->request('GET', '/en/admin/locations/district'); + $crawler = $this->client->request('GET', '/en/admin/locations/district'); - $district = $this->getRepository($client, District::class) + $district = $this->getRepository($this->client, District::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $client->submit($crawler->filter('#delete-district-'.$district)->form()); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $this->client->submit($crawler->filter('#delete-district-'.$district)->form()); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $this->assertNull($this->getRepository($client, District::class) + $this->assertNull($this->getRepository($this->client, District::class) ->findOneBy([ 'slug' => self::SLUG, ])); diff --git a/tests/Functional/Controller/Admin/MetroControllerTest.php b/tests/Functional/Controller/Admin/MetroControllerTest.php index 555ef9e9..ee1fecee 100644 --- a/tests/Functional/Controller/Admin/MetroControllerTest.php +++ b/tests/Functional/Controller/Admin/MetroControllerTest.php @@ -5,35 +5,25 @@ namespace App\Tests\Functional\Controller\Admin; use App\Entity\Metro; -use App\Tests\Helper\WebTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; -final class MetroControllerTest extends WebTestCase +final class MetroControllerTest extends AbstractLocationControllerTest { - use WebTestHelper; - - private const NAME = 'Test'; - private const SLUG = 'test'; - private const EDITED_NAME = 'Edited'; - /** * This test changes the database contents by creating a new Metro station. */ public function testAdminNewStation(): void { - $client = $this->authAsAdmin($this); - - $crawler = $client->request('GET', '/en/admin/locations/metro/new'); + $crawler = $this->client->request('GET', '/en/admin/locations/metro/new'); $form = $crawler->selectButton('Create metro station')->form([ 'metro[name]' => self::NAME, 'metro[slug]' => self::SLUG, ]); - $client->submit($form); + $this->client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); - $station = $this->getRepository($client, Metro::class)->findOneBy([ + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); + $station = $this->getRepository($this->client, Metro::class)->findOneBy([ 'slug' => self::SLUG, ]); @@ -47,23 +37,21 @@ public function testAdminNewStation(): void */ public function testAdminEditStation(): void { - $client = $this->authAsAdmin($this); - - $station = $this->getRepository($client, Metro::class) + $station = $this->getRepository($this->client, Metro::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $crawler = $client->request('GET', '/en/admin/locations/metro/'.$station.'/edit'); + $crawler = $this->client->request('GET', '/en/admin/locations/metro/'.$station.'/edit'); $form = $crawler->selectButton('Save changes')->form([ 'metro[name]' => self::EDITED_NAME, ]); - $client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $this->client->submit($form); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $editedStation = $this->getRepository($client, Metro::class) + $editedStation = $this->getRepository($this->client, Metro::class) ->findOneBy([ 'id' => $station, ]); @@ -76,18 +64,16 @@ public function testAdminEditStation(): void */ public function testAdminDeleteStation(): void { - $client = $this->authAsAdmin($this); - - $station = $this->getRepository($client, Metro::class) + $station = $this->getRepository($this->client, Metro::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $crawler = $client->request('GET', '/en/admin/locations/metro'); - $client->submit($crawler->filter('#delete-metro-'.$station)->form()); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $crawler = $this->client->request('GET', '/en/admin/locations/metro'); + $this->client->submit($crawler->filter('#delete-metro-'.$station)->form()); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $this->assertNull($this->getRepository($client, Metro::class)->findOneBy([ + $this->assertNull($this->getRepository($this->client, Metro::class)->findOneBy([ 'slug' => self::SLUG, ])); } diff --git a/tests/Functional/Controller/Admin/NeighborhoodControllerTest.php b/tests/Functional/Controller/Admin/NeighborhoodControllerTest.php index a336c2ef..9f290f5d 100644 --- a/tests/Functional/Controller/Admin/NeighborhoodControllerTest.php +++ b/tests/Functional/Controller/Admin/NeighborhoodControllerTest.php @@ -5,34 +5,25 @@ namespace App\Tests\Functional\Controller\Admin; use App\Entity\Neighborhood; -use App\Tests\Helper\WebTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; -final class NeighborhoodControllerTest extends WebTestCase +final class NeighborhoodControllerTest extends AbstractLocationControllerTest { - use WebTestHelper; - - private const NAME = 'Test'; - private const SLUG = 'test'; - private const EDITED_NAME = 'Edited'; - /** * This test changes the database contents by creating a new Neighborhood. */ public function testAdminNewNeighborhood(): void { - $client = $this->authAsAdmin($this); - $crawler = $client->request('GET', '/en/admin/locations/neighborhood/new'); + $crawler = $this->client->request('GET', '/en/admin/locations/neighborhood/new'); $form = $crawler->selectButton('Create neighborhood')->form([ 'neighborhood[name]' => self::NAME, 'neighborhood[slug]' => self::SLUG, ]); - $client->submit($form); + $this->client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); - $neighborhood = $this->getRepository($client, Neighborhood::class) + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); + $neighborhood = $this->getRepository($this->client, Neighborhood::class) ->findOneBy([ 'slug' => self::SLUG, ]); @@ -47,23 +38,21 @@ public function testAdminNewNeighborhood(): void */ public function testAdminEditNeighborhood(): void { - $client = $this->authAsAdmin($this); - - $neighborhood = $this->getRepository($client, Neighborhood::class) + $neighborhood = $this->getRepository($this->client, Neighborhood::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $crawler = $client->request('GET', '/en/admin/locations/neighborhood/'.$neighborhood.'/edit'); + $crawler = $this->client->request('GET', '/en/admin/locations/neighborhood/'.$neighborhood.'/edit'); $form = $crawler->selectButton('Save changes')->form([ 'neighborhood[name]' => self::EDITED_NAME, ]); - $client->submit($form); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $this->client->submit($form); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $editedNeighborhood = $this->getRepository($client, Neighborhood::class) + $editedNeighborhood = $this->getRepository($this->client, Neighborhood::class) ->findOneBy([ 'id' => $neighborhood, ]); @@ -76,18 +65,16 @@ public function testAdminEditNeighborhood(): void */ public function testAdminDeleteNeighborhood(): void { - $client = $this->authAsAdmin($this); - - $neighborhood = $this->getRepository($client, Neighborhood::class) + $neighborhood = $this->getRepository($this->client, Neighborhood::class) ->findOneBy([ 'slug' => self::SLUG, ])->getId(); - $crawler = $client->request('GET', '/en/admin/locations/neighborhood'); - $client->submit($crawler->filter('#delete-neighborhood-'.$neighborhood)->form()); - $this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode()); + $crawler = $this->client->request('GET', '/en/admin/locations/neighborhood'); + $this->client->submit($crawler->filter('#delete-neighborhood-'.$neighborhood)->form()); + $this->assertSame(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode()); - $this->assertNull($this->getRepository($client, Neighborhood::class)->findOneBy([ + $this->assertNull($this->getRepository($this->client, Neighborhood::class)->findOneBy([ 'slug' => self::SLUG, ])); } diff --git a/tests/Functional/Controller/Ajax/CityControllerTest.php b/tests/Functional/Controller/Ajax/CityControllerTest.php index ba39c7c2..d8916cd3 100644 --- a/tests/Functional/Controller/Ajax/CityControllerTest.php +++ b/tests/Functional/Controller/Ajax/CityControllerTest.php @@ -7,7 +7,6 @@ use App\Entity\City; use App\Tests\Helper\WebTestHelper; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -use Symfony\Component\HttpFoundation\Response; final class CityControllerTest extends WebTestCase { @@ -48,11 +47,4 @@ public function testSomething(): void $this->assertContainsWords($response, ['Miami', 'South Beach', 'Allapattah']); } - - private function assertContainsWords(Response $response, array $words): void - { - foreach ($words as $word) { - $this->assertStringContainsString($word, (string) $response->getContent()); - } - } } diff --git a/tests/Functional/Controller/User/Security/GoogleAuthenticatorControllerTest.php b/tests/Functional/Controller/User/Security/GoogleAuthenticatorControllerTest.php new file mode 100644 index 00000000..0e76db4b --- /dev/null +++ b/tests/Functional/Controller/User/Security/GoogleAuthenticatorControllerTest.php @@ -0,0 +1,131 @@ +authAsUser($this); + + $client->request('GET', self::ENDPOINT, []); + $this->assertResponseStatusCodeSame(419); + $this->assertJson($client->getResponse()->getContent()); + + $client->request('PUT', self::ENDPOINT, []); + $this->assertResponseStatusCodeSame(419); + $this->assertJson($client->getResponse()->getContent()); + + $client->request('DELETE', self::ENDPOINT, []); + $this->assertResponseStatusCodeSame(419); + $this->assertJson($client->getResponse()->getContent()); + } + + public function testGetAuthCode(): void + { + $client = $this->authAsUser($this); + $token = $this->getToken($client); + $this->assertNotEmpty($token); + $client->request('GET', self::ENDPOINT, [ + 'csrf_token' => $token, + ]); + $this->assertResponseIsSuccessful(); + $response = $client->getResponse(); + $this->assertJson($response->getContent()); + $this->assertTrue(mb_strlen($response->getContent()) > 2000 && mb_strlen($response->getContent()) < 3100); + $this->assertContainsWords($response, ['secret', 'qr_code', 'data:image', 'png;base64']); + } + + /** + * @throws \Exception + */ + public function testSetInvalidAuthCode(): void + { + // Send empty data + $client = $this->authAsUser($this); + $token = $this->getToken($client); + $client->request('PUT', self::ENDPOINT, [ + 'csrf_token' => $token, + ]); + $this->assertResponseIsUnprocessable(); + $response = $client->getResponse(); + $this->assertJson($response->getContent()); + $this->assertContainsWords($response, ['Cannot enable Google Authenticator']); + + // Set wrong data + $client->request('PUT', self::ENDPOINT, [ + 'secret' => self::SECRET, + 'authentication_code' => random_int(100000, 999999), + 'csrf_token' => $token, + ]); + $this->assertResponseIsUnprocessable(); + $response = $client->getResponse(); + $this->assertJson($response->getContent()); + $this->assertContainsWords($response, ['The Google Authenticator code is incorrect or has expired']); + } + + public function testSetAuthCode(): void + { + $client = $this->authAsUser($this); + $token = $this->getToken($client); + + $user = $this->getUser($client, 'user'); + + $user->setGoogleAuthenticatorSecret(self::SECRET); + + $ga = new \PHPGangsta_GoogleAuthenticator(); + $oneTimePassword = $ga->getCode(self::SECRET); + + $client->request('PUT', self::ENDPOINT, [ + 'secret' => self::SECRET, + 'authentication_code' => $oneTimePassword, + 'csrf_token' => $token, + ]); + + $this->assertResponseIsSuccessful(); + + $updatedUser = $this->getRepository($client, User::class)->findOneBy([ + 'username' => 'user', + ]); + + $this->assertTrue($updatedUser->isGoogleAuthenticatorEnabled()); + } + + public function testDeleteAuthCode(): void + { + $client = $this->authAsUser($this); + $token = $this->getToken($client); + + $client->request('DELETE', self::ENDPOINT, [ + 'csrf_token' => $token, + ]); + + $this->assertResponseIsSuccessful(); + + $updatedUser = $this->getRepository($client, User::class)->findOneBy([ + 'username' => 'user', + ]); + + $this->assertFalse($updatedUser->isGoogleAuthenticatorEnabled()); + } + + private function getToken(KernelBrowser $client): string + { + $crawler = $client->request('GET', '/en/user/security'); + + return $crawler->filter('[name="auth_token"]')->attr('value'); + } +} diff --git a/tests/Functional/Controller/User/PasswordControllerTest.php b/tests/Functional/Controller/User/Security/PasswordControllerTest.php similarity index 94% rename from tests/Functional/Controller/User/PasswordControllerTest.php rename to tests/Functional/Controller/User/Security/PasswordControllerTest.php index 4c646a90..c6f9f4dd 100644 --- a/tests/Functional/Controller/User/PasswordControllerTest.php +++ b/tests/Functional/Controller/User/Security/PasswordControllerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Tests\Functional\Controller\User; +namespace App\Tests\Functional\Controller\User\Security; use App\Tests\Helper\WebTestHelper; use Symfony\Bundle\FrameworkBundle\KernelBrowser; @@ -63,7 +63,7 @@ public function testPasswordChange(): void private function getToken(KernelBrowser $client): string { - $crawler = $client->request('GET', '/en/user/property'); + $crawler = $client->request('GET', '/en/user/security'); return $crawler->filter('[name="password_token"]')->attr('value'); } diff --git a/tests/Helper/WebTestHelper.php b/tests/Helper/WebTestHelper.php index 360802ba..fa180d9f 100644 --- a/tests/Helper/WebTestHelper.php +++ b/tests/Helper/WebTestHelper.php @@ -12,6 +12,7 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpFoundation\Response; trait WebTestHelper { @@ -82,4 +83,11 @@ public function resetSettings(KernelBrowser $client): void $this->updateSettings($client, $settings); } + + private function assertContainsWords(Response $response, array $words): void + { + foreach ($words as $word) { + $this->assertStringContainsString($word, (string) $response->getContent()); + } + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 90c3ef8c..0a683767 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -11,3 +11,12 @@ } elseif (method_exists(Dotenv::class, 'bootEnv')) { (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); } + +if (isset($_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'])) { + // executes the "php bin/console cache:clear" command + passthru(sprintf( + 'APP_ENV=%s php "%s/../bin/console" cache:clear --no-warmup', + $_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'], + __DIR__ + )); +} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 06b73d85..8cf165a8 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -54,10 +54,18 @@ menu.profile My Profile + + menu.security + Security + menu.password Change Password + + menu.google_auth + Set up Google Authenticator + menu.logout Log Out @@ -418,6 +426,10 @@ title.my_profile My profile + + title.security + Security + title.my_properties My properties @@ -1102,6 +1114,59 @@ Please wait... + + 2fa.google_authenticator_code + Google Authenticator code + + + 2fa.google_authenticator_code_label + Enter the 6-digit code from Google Authenticator + + + 2fa.buttons.authenticate + Authenticate + + + 2fa.buttons.enable + Enable two-factor authentication + + + 2fa.buttons.disable + Disable + + + 2fa.messages.scan + Please scan the QR code or enter the private key into the App first, then enter the verification code displayed on the Google Authenticator App into the field below. + + + 2fa.messages.enabled + Two-Factor authentication successfully enabled + + + 2fa.messages.disabled + Two-Factor authentication successfully disabled + + + 2fa.messages.active + Two-factor authentication is active. + + + 2fa.errors.secret_is_already_set + The secret is already set + + + 2fa.errors.cannot_enable_ga + Cannot enable Google Authenticator + + + 2fa.errors.cannot_disable_ga + Cannot enable Google Authenticator + + + 2fa.errors.incorrect_ga_code + The Google Authenticator code is incorrect or has expired + + email.new_message New Message From Your Website diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index f26b8a36..92672bc3 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -54,10 +54,18 @@ menu.profile Мой профиль + + menu.security + Безопасность + menu.password Изменить пароль + + menu.google_auth + Настроить Google Authenticator + menu.logout Выйти @@ -418,6 +426,10 @@ title.my_profile Мой профиль + + title.security + Безопасность + title.my_properties Мои объекты @@ -1102,6 +1114,59 @@ Подождите... + + 2fa.google_authenticator_code + Код из Google Authenticator + + + 2fa.google_authenticator_code_label + Введите 6-значный код из приложения Google Authenticator + + + 2fa.buttons.authenticate + Войти + + + 2fa.buttons.enable + Включить двухфакторную аутентификацию + + + 2fa.buttons.disable + Отключить + + + 2fa.messages.scan + Сначала отсканируйте QR-код или введите закрытый ключ в приложение, а затем введите код подтверждения, отображаемый в приложении Google Authenticator, в поле ниже. + + + 2fa.messages.enabled + Двухфакторная аутентификация успешно включена + + + 2fa.messages.disabled + Двухфакторная аутентификация успешно отключена + + + 2fa.messages.active + Двухфакторная аутентификация активна. + + + 2fa.errors.secret_is_already_set + Секрет уже установлен + + + 2fa.errors.cannot_enable_ga + Не удается включить Google Authenticator + + + 2fa.errors.cannot_disable_ga + Не удается отключить Google Authenticator + + + 2fa.errors.incorrect_ga_code + Код Google Authenticator неверен или срок его действия истек + + email.new_message Новое сообщение с вашего сайта diff --git a/webpack.config.js b/webpack.config.js index 3f4a0700..8d914a71 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -31,6 +31,7 @@ Encore .addEntry('js/page', './assets/js/page.js') .addEntry('js/user', './assets/js/user/user.js') .addEntry('js/password', './assets/js/user/password/password.js') + .addEntry('js/google_authenticator', './assets/js/user/two_factor/google_authenticator.js') .addEntry('js/bottom-bar', './assets/js/bottom-bar.js') // will require an extra script tag for runtime.js @@ -73,6 +74,7 @@ Encore .addStyleEntry('css/photo', ['./assets/styles/photo.scss']) .addStyleEntry('css/select2', ['./assets/styles/select2.scss']) .addStyleEntry('css/bottom-bar', ['./assets/styles/bottom-bar.scss']) + .addStyleEntry('css/security', ['./assets/styles/user/security.scss']) //.enableIntegrityHashes() .configureBabel(null, { useBuiltIns: 'usage',