Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed1ab26f1 | ||
|
|
a0dc1a6049 | ||
|
|
3d28485921 |
@@ -12,7 +12,6 @@ RUN apt-get update && \
|
|||||||
ENV APP_DEBUG=0
|
ENV APP_DEBUG=0
|
||||||
ENV APP_ENV=prod
|
ENV APP_ENV=prod
|
||||||
ENV APP_SHARE_DIR=/data/preauth
|
ENV APP_SHARE_DIR=/data/preauth
|
||||||
ENV DEFAULT_URI='http://'
|
|
||||||
|
|
||||||
# load application into build image
|
# load application into build image
|
||||||
RUN mkdir -p /data/preauth
|
RUN mkdir -p /data/preauth
|
||||||
@@ -42,7 +41,6 @@ RUN pecl install apcu && \
|
|||||||
ENV APP_DEBUG=0
|
ENV APP_DEBUG=0
|
||||||
ENV APP_ENV=prod
|
ENV APP_ENV=prod
|
||||||
ENV APP_SHARE_DIR=/data/preauth
|
ENV APP_SHARE_DIR=/data/preauth
|
||||||
ENV DEFAULT_URI='http://'
|
|
||||||
|
|
||||||
# load application into final image
|
# load application into final image
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
+4
-8
@@ -7,14 +7,15 @@
|
|||||||
"php": ">=8.2",
|
"php": ">=8.2",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"bacon/bacon-qr-code": "^3.0",
|
"bacon/bacon-qr-code": "^3.0.3",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
"spomky-labs/otphp": "^11.3",
|
"spomky-labs/otphp": "^11.4.2",
|
||||||
"symfony/cache": "7.4.*",
|
"symfony/cache": "7.4.*",
|
||||||
"symfony/console": "7.4.*",
|
"symfony/console": "7.4.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2.10",
|
||||||
"symfony/framework-bundle": "7.4.*",
|
"symfony/framework-bundle": "7.4.*",
|
||||||
"symfony/mime": "7.4.*",
|
"symfony/mime": "7.4.*",
|
||||||
|
"symfony/rate-limiter": "7.4.*",
|
||||||
"symfony/runtime": "7.4.*",
|
"symfony/runtime": "7.4.*",
|
||||||
"symfony/twig-bundle": "7.4.*",
|
"symfony/twig-bundle": "7.4.*",
|
||||||
"symfony/uid": "7.4.*",
|
"symfony/uid": "7.4.*",
|
||||||
@@ -34,11 +35,6 @@
|
|||||||
"App\\": "src/"
|
"App\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
|
||||||
"psr-4": {
|
|
||||||
"App\\Tests\\": "tests/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"replace": {
|
"replace": {
|
||||||
"symfony/polyfill-ctype": "*",
|
"symfony/polyfill-ctype": "*",
|
||||||
"symfony/polyfill-iconv": "*",
|
"symfony/polyfill-iconv": "*",
|
||||||
|
|||||||
Generated
+290
-156
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "0347dfc31af034e9753433409f0f2a48",
|
"content-hash": "1399aeacc08847b58b9a2df2b729f243",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -484,38 +484,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spomky-labs/otphp",
|
"name": "spomky-labs/otphp",
|
||||||
"version": "11.3.0",
|
"version": "11.4.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Spomky-Labs/otphp.git",
|
"url": "https://github.com/Spomky-Labs/otphp.git",
|
||||||
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33"
|
"reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
|
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad",
|
||||||
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
|
"reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-mbstring": "*",
|
|
||||||
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
|
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
"psr/clock": "^1.0",
|
"psr/clock": "^1.0",
|
||||||
"symfony/deprecation-contracts": "^3.2"
|
"symfony/deprecation-contracts": "^3.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"ekino/phpstan-banned-code": "^1.0",
|
"symfony/error-handler": "^6.4|^7.0|^8.0"
|
||||||
"infection/infection": "^0.26|^0.27|^0.28|^0.29",
|
|
||||||
"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|^10.0|^11.0",
|
|
||||||
"qossmic/deptrac-shim": "^1.0",
|
|
||||||
"rector/rector": "^1.0",
|
|
||||||
"symfony/phpunit-bridge": "^6.1|^7.0",
|
|
||||||
"symplify/easy-coding-standard": "^12.0"
|
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -550,7 +538,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/Spomky-Labs/otphp/issues",
|
"issues": "https://github.com/Spomky-Labs/otphp/issues",
|
||||||
"source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0"
|
"source": "https://github.com/Spomky-Labs/otphp/tree/11.4.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -562,20 +550,20 @@
|
|||||||
"type": "patreon"
|
"type": "patreon"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-06-12T11:22:32+00:00"
|
"time": "2026-01-23T10:53:01+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/cache",
|
"name": "symfony/cache",
|
||||||
"version": "v7.4.1",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/cache.git",
|
"url": "https://github.com/symfony/cache.git",
|
||||||
"reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366"
|
"reference": "665522ec357540e66c294c08583b40ee576574f0"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/cache/zipball/21e0755783bbbab58f2bb6a7a57896d21d27a366",
|
"url": "https://api.github.com/repos/symfony/cache/zipball/665522ec357540e66c294c08583b40ee576574f0",
|
||||||
"reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366",
|
"reference": "665522ec357540e66c294c08583b40ee576574f0",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -646,7 +634,7 @@
|
|||||||
"psr6"
|
"psr6"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/cache/tree/v7.4.1"
|
"source": "https://github.com/symfony/cache/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -666,7 +654,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-04T18:11:45+00:00"
|
"time": "2026-03-06T08:14:57+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/cache-contracts",
|
"name": "symfony/cache-contracts",
|
||||||
@@ -746,16 +734,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/config",
|
"name": "symfony/config",
|
||||||
"version": "v7.4.1",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/config.git",
|
"url": "https://github.com/symfony/config.git",
|
||||||
"reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495"
|
"reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/config/zipball/2c323304c354a43a48b61c5fa760fc4ed60ce495",
|
"url": "https://api.github.com/repos/symfony/config/zipball/6c17162555bfb58957a55bb0e43e00035b6ae3d5",
|
||||||
"reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495",
|
"reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -801,7 +789,7 @@
|
|||||||
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
|
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/config/tree/v7.4.1"
|
"source": "https://github.com/symfony/config/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -821,20 +809,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-05T07:52:08+00:00"
|
"time": "2026-03-06T10:41:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/console",
|
"name": "symfony/console",
|
||||||
"version": "v7.4.1",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/console.git",
|
"url": "https://github.com/symfony/console.git",
|
||||||
"reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e"
|
"reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
|
"url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d",
|
||||||
"reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
|
"reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -899,7 +887,7 @@
|
|||||||
"terminal"
|
"terminal"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/console/tree/v7.4.1"
|
"source": "https://github.com/symfony/console/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -919,20 +907,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-05T15:23:39+00:00"
|
"time": "2026-03-06T14:06:20+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/dependency-injection",
|
"name": "symfony/dependency-injection",
|
||||||
"version": "v7.4.2",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/dependency-injection.git",
|
"url": "https://github.com/symfony/dependency-injection.git",
|
||||||
"reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b"
|
"reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b",
|
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db",
|
||||||
"reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b",
|
"reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -983,7 +971,7 @@
|
|||||||
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
|
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/dependency-injection/tree/v7.4.2"
|
"source": "https://github.com/symfony/dependency-injection/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1003,7 +991,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-08T06:57:04+00:00"
|
"time": "2026-03-03T07:48:48+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/deprecation-contracts",
|
"name": "symfony/deprecation-contracts",
|
||||||
@@ -1074,16 +1062,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/error-handler",
|
"name": "symfony/error-handler",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/error-handler.git",
|
"url": "https://github.com/symfony/error-handler.git",
|
||||||
"reference": "48be2b0653594eea32dcef130cca1c811dcf25c2"
|
"reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2",
|
"url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8",
|
||||||
"reference": "48be2b0653594eea32dcef130cca1c811dcf25c2",
|
"reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1132,7 +1120,7 @@
|
|||||||
"description": "Provides tools to manage errors and ease debugging PHP code",
|
"description": "Provides tools to manage errors and ease debugging PHP code",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/error-handler/tree/v7.4.0"
|
"source": "https://github.com/symfony/error-handler/tree/v7.4.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1152,20 +1140,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-11-05T14:29:59+00:00"
|
"time": "2026-01-20T16:42:42+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher",
|
"name": "symfony/event-dispatcher",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||||
"reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d"
|
"reference": "dc2c0eba1af673e736bb851d747d266108aea746"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d",
|
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746",
|
||||||
"reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d",
|
"reference": "dc2c0eba1af673e736bb851d747d266108aea746",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1217,7 +1205,7 @@
|
|||||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0"
|
"source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1237,7 +1225,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-10-28T09:38:46+00:00"
|
"time": "2026-01-05T11:45:34+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher-contracts",
|
"name": "symfony/event-dispatcher-contracts",
|
||||||
@@ -1317,16 +1305,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/filesystem",
|
"name": "symfony/filesystem",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.6",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/filesystem.git",
|
"url": "https://github.com/symfony/filesystem.git",
|
||||||
"reference": "d551b38811096d0be9c4691d406991b47c0c630a"
|
"reference": "3ebc794fa5315e59fd122561623c2e2e4280538e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a",
|
"url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e",
|
||||||
"reference": "d551b38811096d0be9c4691d406991b47c0c630a",
|
"reference": "3ebc794fa5315e59fd122561623c2e2e4280538e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1363,7 +1351,7 @@
|
|||||||
"description": "Provides basic utilities for the filesystem",
|
"description": "Provides basic utilities for the filesystem",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.0"
|
"source": "https://github.com/symfony/filesystem/tree/v7.4.6"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1383,20 +1371,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-11-27T13:27:24+00:00"
|
"time": "2026-02-25T16:50:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/finder",
|
"name": "symfony/finder",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.6",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/finder.git",
|
"url": "https://github.com/symfony/finder.git",
|
||||||
"reference": "340b9ed7320570f319028a2cbec46d40535e94bd"
|
"reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd",
|
"url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
|
||||||
"reference": "340b9ed7320570f319028a2cbec46d40535e94bd",
|
"reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1431,7 +1419,7 @@
|
|||||||
"description": "Finds files and directories via an intuitive fluent interface",
|
"description": "Finds files and directories via an intuitive fluent interface",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/finder/tree/v7.4.0"
|
"source": "https://github.com/symfony/finder/tree/v7.4.6"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1451,7 +1439,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-11-05T05:42:40+00:00"
|
"time": "2026-01-29T09:40:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/flex",
|
"name": "symfony/flex",
|
||||||
@@ -1528,16 +1516,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/framework-bundle",
|
"name": "symfony/framework-bundle",
|
||||||
"version": "v7.4.1",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/framework-bundle.git",
|
"url": "https://github.com/symfony/framework-bundle.git",
|
||||||
"reference": "2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3"
|
"reference": "c94bc78c85d76af67918404a95d44940f66a7c2f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/framework-bundle/zipball/2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3",
|
"url": "https://api.github.com/repos/symfony/framework-bundle/zipball/c94bc78c85d76af67918404a95d44940f66a7c2f",
|
||||||
"reference": "2fa3b3ad6ed75ce0cc8cad8a5027b4f25b990bc3",
|
"reference": "c94bc78c85d76af67918404a95d44940f66a7c2f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1545,8 +1533,8 @@
|
|||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"php": ">=8.2",
|
"php": ">=8.2",
|
||||||
"symfony/cache": "^6.4.12|^7.0|^8.0",
|
"symfony/cache": "^6.4.12|^7.0|^8.0",
|
||||||
"symfony/config": "^7.4|^8.0",
|
"symfony/config": "^7.4.4|^8.0.4",
|
||||||
"symfony/dependency-injection": "^7.4|^8.0",
|
"symfony/dependency-injection": "^7.4.4|^8.0.4",
|
||||||
"symfony/deprecation-contracts": "^2.5|^3",
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
"symfony/error-handler": "^7.3|^8.0",
|
"symfony/error-handler": "^7.3|^8.0",
|
||||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||||
@@ -1560,8 +1548,8 @@
|
|||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"doctrine/persistence": "<1.3",
|
"doctrine/persistence": "<1.3",
|
||||||
"phpdocumentor/reflection-docblock": "<3.2.2",
|
"phpdocumentor/reflection-docblock": "<5.2|>=7",
|
||||||
"phpdocumentor/type-resolver": "<1.4.0",
|
"phpdocumentor/type-resolver": "<1.5.1",
|
||||||
"symfony/asset": "<6.4",
|
"symfony/asset": "<6.4",
|
||||||
"symfony/asset-mapper": "<6.4",
|
"symfony/asset-mapper": "<6.4",
|
||||||
"symfony/clock": "<6.4",
|
"symfony/clock": "<6.4",
|
||||||
@@ -1593,7 +1581,7 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/persistence": "^1.3|^2|^3",
|
"doctrine/persistence": "^1.3|^2|^3",
|
||||||
"dragonmantank/cron-expression": "^3.1",
|
"dragonmantank/cron-expression": "^3.1",
|
||||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
|
||||||
"seld/jsonlint": "^1.10",
|
"seld/jsonlint": "^1.10",
|
||||||
"symfony/asset": "^6.4|^7.0|^8.0",
|
"symfony/asset": "^6.4|^7.0|^8.0",
|
||||||
"symfony/asset-mapper": "^6.4|^7.0|^8.0",
|
"symfony/asset-mapper": "^6.4|^7.0|^8.0",
|
||||||
@@ -1662,7 +1650,7 @@
|
|||||||
"description": "Provides a tight integration between Symfony components and the Symfony full-stack framework",
|
"description": "Provides a tight integration between Symfony components and the Symfony full-stack framework",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/framework-bundle/tree/v7.4.1"
|
"source": "https://github.com/symfony/framework-bundle/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1682,20 +1670,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-05T14:04:53+00:00"
|
"time": "2026-03-06T15:39:55+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-foundation",
|
"name": "symfony/http-foundation",
|
||||||
"version": "v7.4.1",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/http-foundation.git",
|
"url": "https://github.com/symfony/http-foundation.git",
|
||||||
"reference": "bd1af1e425811d6f077db240c3a588bdb405cd27"
|
"reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27",
|
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81",
|
||||||
"reference": "bd1af1e425811d6f077db240c3a588bdb405cd27",
|
"reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1744,7 +1732,7 @@
|
|||||||
"description": "Defines an object-oriented layer for the HTTP specification",
|
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/http-foundation/tree/v7.4.1"
|
"source": "https://github.com/symfony/http-foundation/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1764,20 +1752,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-07T11:13:10+00:00"
|
"time": "2026-03-06T13:15:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-kernel",
|
"name": "symfony/http-kernel",
|
||||||
"version": "v7.4.2",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/http-kernel.git",
|
"url": "https://github.com/symfony/http-kernel.git",
|
||||||
"reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f"
|
"reference": "3b3fcf386c809be990c922e10e4c620d6367cab1"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f",
|
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1",
|
||||||
"reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f",
|
"reference": "3b3fcf386c809be990c922e10e4c620d6367cab1",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1819,7 +1807,7 @@
|
|||||||
"symfony/config": "^6.4|^7.0|^8.0",
|
"symfony/config": "^6.4|^7.0|^8.0",
|
||||||
"symfony/console": "^6.4|^7.0|^8.0",
|
"symfony/console": "^6.4|^7.0|^8.0",
|
||||||
"symfony/css-selector": "^6.4|^7.0|^8.0",
|
"symfony/css-selector": "^6.4|^7.0|^8.0",
|
||||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
"symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0",
|
||||||
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
|
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
|
||||||
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||||
@@ -1863,7 +1851,7 @@
|
|||||||
"description": "Provides a structured process for converting a Request into a Response",
|
"description": "Provides a structured process for converting a Request into a Response",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/http-kernel/tree/v7.4.2"
|
"source": "https://github.com/symfony/http-kernel/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1883,20 +1871,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-08T07:43:37+00:00"
|
"time": "2026-03-06T16:33:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/mime",
|
"name": "symfony/mime",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/mime.git",
|
"url": "https://github.com/symfony/mime.git",
|
||||||
"reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a"
|
"reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a",
|
"url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1",
|
||||||
"reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a",
|
"reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1907,15 +1895,15 @@
|
|||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"egulias/email-validator": "~3.0.0",
|
"egulias/email-validator": "~3.0.0",
|
||||||
"phpdocumentor/reflection-docblock": "<3.2.2",
|
"phpdocumentor/reflection-docblock": "<5.2|>=7",
|
||||||
"phpdocumentor/type-resolver": "<1.4.0",
|
"phpdocumentor/type-resolver": "<1.5.1",
|
||||||
"symfony/mailer": "<6.4",
|
"symfony/mailer": "<6.4",
|
||||||
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
|
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"egulias/email-validator": "^2.1.10|^3.1|^4",
|
"egulias/email-validator": "^2.1.10|^3.1|^4",
|
||||||
"league/html-to-markdown": "^5.0",
|
"league/html-to-markdown": "^5.0",
|
||||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
|
||||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
"symfony/process": "^6.4|^7.0|^8.0",
|
"symfony/process": "^6.4|^7.0|^8.0",
|
||||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||||
@@ -1952,7 +1940,7 @@
|
|||||||
"mime-type"
|
"mime-type"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/mime/tree/v7.4.0"
|
"source": "https://github.com/symfony/mime/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1972,7 +1960,78 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-11-16T10:14:42+00:00"
|
"time": "2026-03-05T15:24:09+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/options-resolver",
|
||||||
|
"version": "v7.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/options-resolver.git",
|
||||||
|
"reference": "b38026df55197f9e39a44f3215788edf83187b80"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80",
|
||||||
|
"reference": "b38026df55197f9e39a44f3215788edf83187b80",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\OptionsResolver\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides an improved replacement for the array_replace PHP function",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"config",
|
||||||
|
"configuration",
|
||||||
|
"options"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/options-resolver/tree/v7.4.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-12T15:39:26+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-grapheme",
|
"name": "symfony/polyfill-intl-grapheme",
|
||||||
@@ -2477,17 +2536,91 @@
|
|||||||
"time": "2024-09-09T11:45:10+00:00"
|
"time": "2024-09-09T11:45:10+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/routing",
|
"name": "symfony/rate-limiter",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/routing.git",
|
"url": "https://github.com/symfony/rate-limiter.git",
|
||||||
"reference": "4720254cb2644a0b876233d258a32bf017330db7"
|
"reference": "c2ff01c8d5ed54f0721f046fde14a94f2df09666"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7",
|
"url": "https://api.github.com/repos/symfony/rate-limiter/zipball/c2ff01c8d5ed54f0721f046fde14a94f2df09666",
|
||||||
"reference": "4720254cb2644a0b876233d258a32bf017330db7",
|
"reference": "c2ff01c8d5ed54f0721f046fde14a94f2df09666",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/options-resolver": "^7.3|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"psr/cache": "^1.0|^2.0|^3.0",
|
||||||
|
"symfony/lock": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\RateLimiter\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Wouter de Jong",
|
||||||
|
"email": "wouter@wouterj.nl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides a Token Bucket implementation to rate limit input and output in your application",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"limiter",
|
||||||
|
"rate-limiter"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/rate-limiter/tree/v7.4.7"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-04T13:54:41+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/routing",
|
||||||
|
"version": "v7.4.6",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/routing.git",
|
||||||
|
"reference": "238d749c56b804b31a9bf3e26519d93b65a60938"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938",
|
||||||
|
"reference": "238d749c56b804b31a9bf3e26519d93b65a60938",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2539,7 +2672,7 @@
|
|||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/routing/tree/v7.4.0"
|
"source": "https://github.com/symfony/routing/tree/v7.4.6"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2559,7 +2692,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-11-27T13:27:24+00:00"
|
"time": "2026-02-25T16:50:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/runtime",
|
"name": "symfony/runtime",
|
||||||
@@ -2733,16 +2866,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/string",
|
"name": "symfony/string",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.6",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/string.git",
|
"url": "https://github.com/symfony/string.git",
|
||||||
"reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003"
|
"reference": "9f209231affa85aa930a5e46e6eb03381424b30b"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003",
|
"url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b",
|
||||||
"reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003",
|
"reference": "9f209231affa85aa930a5e46e6eb03381424b30b",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2800,7 +2933,7 @@
|
|||||||
"utf8"
|
"utf8"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/string/tree/v7.4.0"
|
"source": "https://github.com/symfony/string/tree/v7.4.6"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2820,7 +2953,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-11-27T13:27:24+00:00"
|
"time": "2026-02-09T09:33:46+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/translation-contracts",
|
"name": "symfony/translation-contracts",
|
||||||
@@ -2906,16 +3039,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/twig-bridge",
|
"name": "symfony/twig-bridge",
|
||||||
"version": "v7.4.1",
|
"version": "v7.4.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/twig-bridge.git",
|
"url": "https://github.com/symfony/twig-bridge.git",
|
||||||
"reference": "9103559ef3e9f06708d8bff6810f6335b8f1eee8"
|
"reference": "c67219ca6b79a57b64e36bbb2cd8ba741286587e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/9103559ef3e9f06708d8bff6810f6335b8f1eee8",
|
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/c67219ca6b79a57b64e36bbb2cd8ba741286587e",
|
||||||
"reference": "9103559ef3e9f06708d8bff6810f6335b8f1eee8",
|
"reference": "c67219ca6b79a57b64e36bbb2cd8ba741286587e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2925,10 +3058,10 @@
|
|||||||
"twig/twig": "^3.21"
|
"twig/twig": "^3.21"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"phpdocumentor/reflection-docblock": "<3.2.2",
|
"phpdocumentor/reflection-docblock": "<5.2|>=7",
|
||||||
"phpdocumentor/type-resolver": "<1.4.0",
|
"phpdocumentor/type-resolver": "<1.5.1",
|
||||||
"symfony/console": "<6.4",
|
"symfony/console": "<6.4",
|
||||||
"symfony/form": "<6.4",
|
"symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4",
|
||||||
"symfony/http-foundation": "<6.4",
|
"symfony/http-foundation": "<6.4",
|
||||||
"symfony/http-kernel": "<6.4",
|
"symfony/http-kernel": "<6.4",
|
||||||
"symfony/mime": "<6.4",
|
"symfony/mime": "<6.4",
|
||||||
@@ -2939,7 +3072,7 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"egulias/email-validator": "^2.1.10|^3|^4",
|
"egulias/email-validator": "^2.1.10|^3|^4",
|
||||||
"league/html-to-markdown": "^5.0",
|
"league/html-to-markdown": "^5.0",
|
||||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
|
||||||
"symfony/asset": "^6.4|^7.0|^8.0",
|
"symfony/asset": "^6.4|^7.0|^8.0",
|
||||||
"symfony/asset-mapper": "^6.4|^7.0|^8.0",
|
"symfony/asset-mapper": "^6.4|^7.0|^8.0",
|
||||||
"symfony/console": "^6.4|^7.0|^8.0",
|
"symfony/console": "^6.4|^7.0|^8.0",
|
||||||
@@ -2947,7 +3080,7 @@
|
|||||||
"symfony/emoji": "^7.1|^8.0",
|
"symfony/emoji": "^7.1|^8.0",
|
||||||
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||||
"symfony/form": "^6.4.30|~7.3.8|^7.4.1|^8.0.1",
|
"symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
|
||||||
"symfony/html-sanitizer": "^6.4|^7.0|^8.0",
|
"symfony/html-sanitizer": "^6.4|^7.0|^8.0",
|
||||||
"symfony/http-foundation": "^7.3|^8.0",
|
"symfony/http-foundation": "^7.3|^8.0",
|
||||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||||
@@ -2997,7 +3130,7 @@
|
|||||||
"description": "Provides integration for Twig with various Symfony components",
|
"description": "Provides integration for Twig with various Symfony components",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/twig-bridge/tree/v7.4.1"
|
"source": "https://github.com/symfony/twig-bridge/tree/v7.4.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3017,20 +3150,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-05T14:04:53+00:00"
|
"time": "2026-03-04T15:37:05+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/twig-bundle",
|
"name": "symfony/twig-bundle",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/twig-bundle.git",
|
"url": "https://github.com/symfony/twig-bundle.git",
|
||||||
"reference": "f83f530d00d1bbc6f7fafeb433077887c83326ef"
|
"reference": "e8829e02ff96a391ed0703bac9e7ff0537480b6b"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/twig-bundle/zipball/f83f530d00d1bbc6f7fafeb433077887c83326ef",
|
"url": "https://api.github.com/repos/symfony/twig-bundle/zipball/e8829e02ff96a391ed0703bac9e7ff0537480b6b",
|
||||||
"reference": "f83f530d00d1bbc6f7fafeb433077887c83326ef",
|
"reference": "e8829e02ff96a391ed0703bac9e7ff0537480b6b",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3038,6 +3171,7 @@
|
|||||||
"php": ">=8.2",
|
"php": ">=8.2",
|
||||||
"symfony/config": "^7.4|^8.0",
|
"symfony/config": "^7.4|^8.0",
|
||||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||||
"symfony/http-kernel": "^6.4.13|^7.1.6|^8.0",
|
"symfony/http-kernel": "^6.4.13|^7.1.6|^8.0",
|
||||||
"symfony/twig-bridge": "^7.3|^8.0",
|
"symfony/twig-bridge": "^7.3|^8.0",
|
||||||
@@ -3086,7 +3220,7 @@
|
|||||||
"description": "Provides a tight integration of Twig into the Symfony full-stack framework",
|
"description": "Provides a tight integration of Twig into the Symfony full-stack framework",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/twig-bundle/tree/v7.4.0"
|
"source": "https://github.com/symfony/twig-bundle/tree/v7.4.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3106,20 +3240,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-10-02T07:41:02+00:00"
|
"time": "2026-01-06T12:34:24+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/uid",
|
"name": "symfony/uid",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/uid.git",
|
"url": "https://github.com/symfony/uid.git",
|
||||||
"reference": "2498e9f81b7baa206f44de583f2f48350b90142c"
|
"reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c",
|
"url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36",
|
||||||
"reference": "2498e9f81b7baa206f44de583f2f48350b90142c",
|
"reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3164,7 +3298,7 @@
|
|||||||
"uuid"
|
"uuid"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/uid/tree/v7.4.0"
|
"source": "https://github.com/symfony/uid/tree/v7.4.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3184,20 +3318,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-09-25T11:02:55+00:00"
|
"time": "2026-01-03T23:30:35+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/var-dumper",
|
"name": "symfony/var-dumper",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.6",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/var-dumper.git",
|
"url": "https://github.com/symfony/var-dumper.git",
|
||||||
"reference": "41fd6c4ae28c38b294b42af6db61446594a0dece"
|
"reference": "045321c440ac18347b136c63d2e9bf28a2dc0291"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece",
|
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291",
|
||||||
"reference": "41fd6c4ae28c38b294b42af6db61446594a0dece",
|
"reference": "045321c440ac18347b136c63d2e9bf28a2dc0291",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3251,7 +3385,7 @@
|
|||||||
"dump"
|
"dump"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/var-dumper/tree/v7.4.0"
|
"source": "https://github.com/symfony/var-dumper/tree/v7.4.6"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3271,7 +3405,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-10-27T20:36:44+00:00"
|
"time": "2026-02-15T10:53:20+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/var-exporter",
|
"name": "symfony/var-exporter",
|
||||||
@@ -3356,16 +3490,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/yaml",
|
"name": "symfony/yaml",
|
||||||
"version": "v7.4.1",
|
"version": "v7.4.6",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/yaml.git",
|
"url": "https://github.com/symfony/yaml.git",
|
||||||
"reference": "24dd4de28d2e3988b311751ac49e684d783e2345"
|
"reference": "58751048de17bae71c5aa0d13cb19d79bca26391"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345",
|
"url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391",
|
||||||
"reference": "24dd4de28d2e3988b311751ac49e684d783e2345",
|
"reference": "58751048de17bae71c5aa0d13cb19d79bca26391",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3408,7 +3542,7 @@
|
|||||||
"description": "Loads and dumps YAML files",
|
"description": "Loads and dumps YAML files",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/yaml/tree/v7.4.1"
|
"source": "https://github.com/symfony/yaml/tree/v7.4.6"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3428,20 +3562,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-04T18:11:45+00:00"
|
"time": "2026-02-09T09:33:46+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "twig/twig",
|
"name": "twig/twig",
|
||||||
"version": "v3.22.1",
|
"version": "v3.23.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/twigphp/Twig.git",
|
"url": "https://github.com/twigphp/Twig.git",
|
||||||
"reference": "1de2ec1fc43ab58a4b7e80b214b96bfc895750f3"
|
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/1de2ec1fc43ab58a4b7e80b214b96bfc895750f3",
|
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||||
"reference": "1de2ec1fc43ab58a4b7e80b214b96bfc895750f3",
|
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3495,7 +3629,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/twigphp/Twig/issues",
|
"issues": "https://github.com/twigphp/Twig/issues",
|
||||||
"source": "https://github.com/twigphp/Twig/tree/v3.22.1"
|
"source": "https://github.com/twigphp/Twig/tree/v3.23.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3507,7 +3641,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-11-16T16:01:12+00:00"
|
"time": "2026-01-23T21:00:41+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|||||||
@@ -2,38 +2,14 @@ framework:
|
|||||||
cache:
|
cache:
|
||||||
app: cache.adapter.filesystem
|
app: cache.adapter.filesystem
|
||||||
pools:
|
pools:
|
||||||
noncePool:
|
nonceCache:
|
||||||
adapters:
|
adapters: cache.adapter.apcu
|
||||||
- cache.adapter.apcu
|
rateLimitCache:
|
||||||
sessionPool:
|
adapters: cache.adapter.apcu
|
||||||
adapters:
|
sessionCache:
|
||||||
- cache.adapter.apcu
|
adapters: cache.adapter.apcu
|
||||||
requestPool:
|
sessionStorage:
|
||||||
adapters:
|
adapters: cache.adapter.filesystem
|
||||||
- cache.adapter.apcu
|
|
||||||
persistSessionPool:
|
|
||||||
adapters:
|
|
||||||
- cache.adapter.filesystem
|
|
||||||
persistRequestPool:
|
|
||||||
adapters:
|
|
||||||
- cache.adapter.filesystem
|
|
||||||
|
|
||||||
# Unique name of your app: used to compute
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
# stable namespaces for cache keys.
|
|
||||||
prefix_seed: digitaladapt/preauth
|
prefix_seed: digitaladapt/preauth
|
||||||
|
|
||||||
# The "app" cache stores to the filesystem by default.
|
|
||||||
# The data in this cache should persist between deploys.
|
|
||||||
# Other options include:
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
#app: cache.adapter.redis
|
|
||||||
#default_redis_provider: redis://localhost
|
|
||||||
|
|
||||||
# APCu (not recommended with heavy random-write workloads
|
|
||||||
# as memory fragmentation can cause perf issues)
|
|
||||||
#app: cache.adapter.apcu
|
|
||||||
|
|
||||||
# Namespaced pools use the above "app" backend by default
|
|
||||||
#pools:
|
|
||||||
#my.dedicated.cache: null
|
|
||||||
|
|||||||
@@ -7,12 +7,3 @@ framework:
|
|||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
session: true
|
session: true
|
||||||
|
|
||||||
#esi: true
|
|
||||||
#fragments: true
|
|
||||||
|
|
||||||
when@test:
|
|
||||||
framework:
|
|
||||||
test: true
|
|
||||||
session:
|
|
||||||
storage_factory_id: session.storage.factory.mock_file
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
framework:
|
||||||
|
rate_limiter:
|
||||||
|
burst:
|
||||||
|
policy: 'sliding_window'
|
||||||
|
limit: '%env(int:BURST_COUNT)%'
|
||||||
|
interval: '%env(int:BURST_TIME)% seconds'
|
||||||
|
cache_pool: 'rateLimitCache'
|
||||||
|
upper:
|
||||||
|
policy: 'sliding_window'
|
||||||
|
limit: '%env(int:UPPER_COUNT)%'
|
||||||
|
interval: '%env(int:UPPER_TIME)% seconds'
|
||||||
|
cache_pool: 'rateLimitCache'
|
||||||
|
login_limiter:
|
||||||
|
policy: compound
|
||||||
|
limiters: [burst, upper]
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
framework:
|
framework:
|
||||||
router:
|
router:
|
||||||
# Configure how to generate URLs in non-HTTP contexts,
|
default_uri: 'http://localhost'
|
||||||
# such as CLI commands. See
|
|
||||||
# https://symfony.com/doc/current/routing.html
|
|
||||||
# #generating-urls-in-commands
|
|
||||||
default_uri: '%env(DEFAULT_URI)%'
|
|
||||||
|
|
||||||
when@prod:
|
when@prod:
|
||||||
framework:
|
framework:
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ twig:
|
|||||||
strict_variables: true
|
strict_variables: true
|
||||||
globals:
|
globals:
|
||||||
env:
|
env:
|
||||||
allow_password: '%env(STATIC_SECRET_ENABLED)%'
|
|
||||||
title: '%env(TITLE)%'
|
title: '%env(TITLE)%'
|
||||||
bg_color: '%env(BG_COLOR)%'
|
bg_color: '%env(BG_COLOR)%'
|
||||||
fg_color: '%env(FG_COLOR)%'
|
fg_color: '%env(FG_COLOR)%'
|
||||||
|
|||||||
+404
-407
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
|
||||||
|
|
||||||
# This file is the entry point to configure the routes of your app.
|
|
||||||
# Methods with the #[Route] attribute are automatically imported.
|
|
||||||
# See also https://symfony.com/doc/current/routing.html
|
|
||||||
|
|
||||||
# To list all registered routes, run the following command:
|
|
||||||
# bin/console debug:router
|
|
||||||
|
|
||||||
controllers:
|
|
||||||
resource: routing.controllers
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
when@dev:
|
|
||||||
_errors:
|
|
||||||
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
|
||||||
prefix: /_error
|
|
||||||
+5
-16
@@ -16,12 +16,11 @@ parameters:
|
|||||||
# how long will someone stay logged in, measured in seconds, zero for DEFAULT
|
# how long will someone stay logged in, measured in seconds, zero for DEFAULT
|
||||||
env(COOKIE_TTL): '2592000' # default 30 days
|
env(COOKIE_TTL): '2592000' # default 30 days
|
||||||
# rate limiting can *NOT* be disabled, but you could allow hundreds of logins a second
|
# rate limiting can *NOT* be disabled, but you could allow hundreds of logins a second
|
||||||
# number of consecutive failed login attempts before we block the ip address
|
# default is the lower of 2 per half-minute or 10 per hour
|
||||||
env(LIMIT): '4' # default 4 failed login attempts before blocking
|
env(BURST_COUNT): 2 # 2 per 30 seconds
|
||||||
# time between failed login attempts that are consecutive, in seconds, zero for DEFAULT
|
env(BURST_TIME): 30 # seconds
|
||||||
env(LIMIT_TIMEOUT): '21600' # default 6 hours
|
env(UPPER_COUNT): 10 # 10 per hour
|
||||||
# how long a blocked ip address stay blocks, in seconds, zero for DEFAULT
|
env(UPPER_TIME): 3600 # seconds (1 hour)
|
||||||
env(LIMIT_TTL): '86400' # default 24 hours
|
|
||||||
|
|
||||||
# --- extra variables ---
|
# --- extra variables ---
|
||||||
# query parameter prefix to prevent collisions
|
# query parameter prefix to prevent collisions
|
||||||
@@ -29,10 +28,6 @@ parameters:
|
|||||||
# how long do we allow all traffic from an ip address after successful login
|
# how long do we allow all traffic from an ip address after successful login
|
||||||
# could be useful if you have a system which does not handle cookies
|
# could be useful if you have a system which does not handle cookies
|
||||||
env(IP_TTL): '0' # default disabled, time in seconds
|
env(IP_TTL): '0' # default disabled, time in seconds
|
||||||
# if desired, in addition to supporting a TOTP, you can set a static password
|
|
||||||
env(STATIC_SECRET_ENABLED): '0' # boolean
|
|
||||||
env(STATIC_SECRET): '' # default disabled
|
|
||||||
env(LOOKUP_TOTP): '' # default disabled
|
|
||||||
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
||||||
env(TEAPOT): '1' # boolean
|
env(TEAPOT): '1' # boolean
|
||||||
|
|
||||||
@@ -54,16 +49,10 @@ parameters:
|
|||||||
env(TOO_MANY_MESSAGE): 'Try again later'
|
env(TOO_MANY_MESSAGE): 'Try again later'
|
||||||
|
|
||||||
app.cookie_ttl: '%env(COOKIE_TTL)%'
|
app.cookie_ttl: '%env(COOKIE_TTL)%'
|
||||||
app.limit: '%env(LIMIT)%'
|
|
||||||
app.limit_timeout: '%env(LIMIT_TIMEOUT)%'
|
|
||||||
app.limit_ttl: '%env(LIMIT_TTL)%'
|
|
||||||
app.query_prefix: '%env(QUERY_PREFIX)%'
|
app.query_prefix: '%env(QUERY_PREFIX)%'
|
||||||
app.totp_uri: '%env(TOTP_URI)%'
|
app.totp_uri: '%env(TOTP_URI)%'
|
||||||
|
|
||||||
app.ip_ttl: '%env(IP_TTL)%'
|
app.ip_ttl: '%env(IP_TTL)%'
|
||||||
app.static_secret_enabled: '%env(STATIC_SECRET_ENABLED)%'
|
|
||||||
app.static_secret: '%env(STATIC_SECRET)%'
|
|
||||||
app.lookup_totp: '%env(LOOKUP_TOTP)%'
|
|
||||||
app.teapot: '%env(TEAPOT)%'
|
app.teapot: '%env(TEAPOT)%'
|
||||||
|
|
||||||
app.error_message: '%env(ERROR_MESSAGE)%'
|
app.error_message: '%env(ERROR_MESSAGE)%'
|
||||||
|
|||||||
+5
-16
@@ -11,14 +11,11 @@
|
|||||||
# NOTE: rate limiting can *NOT* be disabled,
|
# NOTE: rate limiting can *NOT* be disabled,
|
||||||
# but you could allow hundreds of logins a second
|
# but you could allow hundreds of logins a second
|
||||||
|
|
||||||
# number of consecutive failed login attempts before we block the ip address
|
# rate limiting, default is the lower of 2 per 30 seconds or 10 per hour
|
||||||
#LIMIT=4 # default 4 failed login attempts before blocking
|
#BURST_COUNT=2 # 2 per 30 seconds
|
||||||
|
#BURST_TIME=30 # seconds
|
||||||
# time between failed login attempts that are consecutive, in seconds, zero for DEFAULT
|
#UPPER_COUNT=10 # 10 per hour
|
||||||
#LIMIT_TIMEOUT=21600 # default 6 hours
|
#UPPER_TIME=3600 # seconds (1 hour)
|
||||||
|
|
||||||
# how long a blocked ip address stay blocks, in seconds, zero for DEFAULT
|
|
||||||
#LIMIT_TTL=86400 # default 24 hours
|
|
||||||
|
|
||||||
# --- Extra Options ---
|
# --- Extra Options ---
|
||||||
|
|
||||||
@@ -26,14 +23,6 @@
|
|||||||
# could be useful if you have a system which does not handle cookies
|
# could be useful if you have a system which does not handle cookies
|
||||||
#IP_TTL=0 # default disabled, time in seconds
|
#IP_TTL=0 # default disabled, time in seconds
|
||||||
|
|
||||||
# if desired, in addition to supporting a TOTP, you can set a static password
|
|
||||||
# if you enable a static secret, you may choose to also enable a totp-lookup,
|
|
||||||
# which contains a special path, when successfully authenticated,
|
|
||||||
# instead of returning "hi <your-name>" it returns "code <totp-code>".
|
|
||||||
#STATIC_SECRET_ENABLED='0' # boolean, disabled by default
|
|
||||||
#STATIC_SECRET='' # default disabled
|
|
||||||
#LOOKUP_TOTP='' # default disabled
|
|
||||||
|
|
||||||
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
||||||
#TEAPOT=true # default enabled, boolean
|
#TEAPOT=true # default enabled, boolean
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ There is an example Caddyfile in /docs/ and env.example file to get you started.
|
|||||||
|
|
||||||
When someone tries to reach your protected web service, Caddy will check with preauth if they are allowed, if their preauth cookie is missing, invalid, or expired, we will show them to a login screen.
|
When someone tries to reach your protected web service, Caddy will check with preauth if they are allowed, if their preauth cookie is missing, invalid, or expired, we will show them to a login screen.
|
||||||
|
|
||||||
I say login, but it's really just a TOTP code (6 digit code which changes every 30 second). But once they enter the right code,they'll get their cookie and be shown the protected service. It is also possible to allow all requests from an approved IP address, but that is disabled by default.
|
I say login, but it's really just a TOTP code (6-digit code which changes every 30 second). But once they enter the right code,they'll get their cookie and be shown the protected service. It is also possible to allow all requests from an approved IP address, but that is disabled by default.
|
||||||
|
|
||||||
First time you spin up the docker container it will generate a TOTP secret (which you'll load into your authenticator app); or generate you own.
|
First time you spin up the docker container it will generate a TOTP secret (which you'll load into your authenticator app); or generate you own.
|
||||||
|
|
||||||
Be sure to save that TOTP secret to your docker environment, so that it persistents beyond removing the container.
|
Be sure to save that TOTP secret to your docker environment, so that it persists beyond removing the container.
|
||||||
|
|
||||||
### History
|
### History
|
||||||
#### v0.4.1 (Dec 26th, 2025)
|
#### v0.4.1 (Dec 26th, 2025)
|
||||||
|
|||||||
@@ -10,14 +10,9 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|||||||
final readonly class ConfigBag {
|
final readonly class ConfigBag {
|
||||||
private ClockInterface $clock;
|
private ClockInterface $clock;
|
||||||
private int $cookieTtl;
|
private int $cookieTtl;
|
||||||
private int $limit;
|
|
||||||
private int $limitTimeout;
|
|
||||||
private int $limitTtl;
|
|
||||||
private string $queryPrefix;
|
private string $queryPrefix;
|
||||||
private string $totpUri;
|
private string $totpUri;
|
||||||
private ?int $ipTtl;
|
private ?int $ipTtl;
|
||||||
private ?string $lookupTotp;
|
|
||||||
private ?string $staticSecret;
|
|
||||||
private bool $teapot;
|
private bool $teapot;
|
||||||
private string $errorMessage;
|
private string $errorMessage;
|
||||||
private string $teapotTitle;
|
private string $teapotTitle;
|
||||||
@@ -28,15 +23,9 @@ final readonly class ConfigBag {
|
|||||||
Utilities $utilities,
|
Utilities $utilities,
|
||||||
ClockInterface $clock,
|
ClockInterface $clock,
|
||||||
#[Autowire('%app.cookie_ttl%')] int $cookieTtl,
|
#[Autowire('%app.cookie_ttl%')] int $cookieTtl,
|
||||||
#[Autowire('%app.limit%')] int $limit,
|
|
||||||
#[Autowire('%app.limit_timeout%')] int $limitTimeout,
|
|
||||||
#[Autowire('%app.limit_ttl%')] int $limitTtl,
|
|
||||||
#[Autowire('%app.query_prefix%')] string $queryPrefix,
|
#[Autowire('%app.query_prefix%')] string $queryPrefix,
|
||||||
#[Autowire('%app.totp_uri%')] string $totpUri,
|
#[Autowire('%app.totp_uri%')] string $totpUri,
|
||||||
#[Autowire('%app.ip_ttl%')] ?int $ipTtl,
|
#[Autowire('%app.ip_ttl%')] ?int $ipTtl,
|
||||||
#[Autowire('%app.lookup_totp%')] ?string $lookupTotp,
|
|
||||||
#[Autowire('%app.static_secret_enabled%')] bool $staticSecretEnabled,
|
|
||||||
#[Autowire('%app.static_secret%')] ?string $staticSecret,
|
|
||||||
#[Autowire('%app.teapot%')] bool $teapot,
|
#[Autowire('%app.teapot%')] bool $teapot,
|
||||||
#[Autowire('%app.error_message%')] string $errorMessage,
|
#[Autowire('%app.error_message%')] string $errorMessage,
|
||||||
#[Autowire('%app.teapot_title%')] string $teapotTitle,
|
#[Autowire('%app.teapot_title%')] string $teapotTitle,
|
||||||
@@ -44,14 +33,9 @@ final readonly class ConfigBag {
|
|||||||
) {
|
) {
|
||||||
$this->clock = $clock;
|
$this->clock = $clock;
|
||||||
$this->cookieTtl = $cookieTtl;
|
$this->cookieTtl = $cookieTtl;
|
||||||
$this->limit = ($limit >= 1) ? $limit : 4;
|
|
||||||
$this->limitTimeout = ($limitTimeout >= 1) ? $limitTimeout : 21600;
|
|
||||||
$this->limitTtl = ($limitTtl >= 1) ? $limitTtl : 86400;
|
|
||||||
$this->queryPrefix = $queryPrefix;
|
$this->queryPrefix = $queryPrefix;
|
||||||
$this->totpUri = $totpUri ?: $utilities->loadTotp();
|
$this->totpUri = $totpUri ?: $utilities->loadTotp();
|
||||||
$this->ipTtl = $ipTtl ?: null;
|
$this->ipTtl = $ipTtl ?: null;
|
||||||
$this->staticSecret = $staticSecretEnabled ? ($staticSecret ?: null) : null;
|
|
||||||
$this->lookupTotp = $this->staticSecret ? ($lookupTotp ?: null) : null;
|
|
||||||
$this->teapot = $teapot;
|
$this->teapot = $teapot;
|
||||||
$this->errorMessage = $errorMessage;
|
$this->errorMessage = $errorMessage;
|
||||||
$this->teapotTitle = $teapotTitle;
|
$this->teapotTitle = $teapotTitle;
|
||||||
@@ -66,18 +50,6 @@ final readonly class ConfigBag {
|
|||||||
return $this->cookieTtl;
|
return $this->cookieTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function limit(): int {
|
|
||||||
return $this->limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function limitTimeout(): int {
|
|
||||||
return $this->limitTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function limitTtl(): int {
|
|
||||||
return $this->limitTtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function query(string $field): string {
|
public function query(string $field): string {
|
||||||
return "$this->queryPrefix$field";
|
return "$this->queryPrefix$field";
|
||||||
}
|
}
|
||||||
@@ -90,14 +62,6 @@ final readonly class ConfigBag {
|
|||||||
return $this->ipTtl;
|
return $this->ipTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lookupTotp(): ?string {
|
|
||||||
return $this->lookupTotp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function staticSecret(): ?string {
|
|
||||||
return $this->staticSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function teapot(): bool {
|
public function teapot(): bool {
|
||||||
return $this->teapot;
|
return $this->teapot;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-24
@@ -8,12 +8,11 @@ use App\Enum\Scope;
|
|||||||
/* When scope is Ip but ip-access is disabled, scope is to be considered Cookie. */
|
/* When scope is Ip but ip-access is disabled, scope is to be considered Cookie. */
|
||||||
/* When using password but password is disabled, request will always fail. */
|
/* When using password but password is disabled, request will always fail. */
|
||||||
final class Payload {
|
final class Payload {
|
||||||
public string $id; /* session name, identifying who is logging in */
|
public string $id; /* session name, identifying who is logging in */
|
||||||
public ?string $token; /* totp, typically six digits */
|
public string $token; /* totp, typically six digits */
|
||||||
public ?string $password; /* static secret, alternative to token, if enabled */
|
public string $nonce; /* random unique string, to block duplicate submissions */
|
||||||
public string $nonce; /* random unique string, to block duplicate submissions */
|
public bool $json; /* should we return json (for the login page) */
|
||||||
public bool $json; /* should we return json (for the login page) */
|
public Scope $scope; /* type of access being requested */
|
||||||
public Scope $scope; /* type of access being requested */
|
|
||||||
|
|
||||||
public static function decode(string $base64url): ?Payload {
|
public static function decode(string $base64url): ?Payload {
|
||||||
/* convert the base64url into json string */
|
/* convert the base64url into json string */
|
||||||
@@ -31,12 +30,10 @@ final class Payload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function create(object $data): ?Payload {
|
public static function create(object $data): ?Payload {
|
||||||
/* if missing required fields id or nonce */
|
/* if missing required fields id, nonce, or token */
|
||||||
if (strlen($data->id ?? '') < 1 ||
|
if (strlen($data->id ?? '') < 1 ||
|
||||||
strlen($data->nonce ?? '') < 1 ||
|
strlen($data->nonce ?? '') < 1 ||
|
||||||
/* if missing both token and password (we require one of them) */
|
strlen($data->token ?? '') < 1
|
||||||
(strlen($data->token ?? '') < 1 &&
|
|
||||||
strlen($data->password ?? '') < 1)
|
|
||||||
) {
|
) {
|
||||||
/* returns null as the input is invalid */
|
/* returns null as the input is invalid */
|
||||||
return null;
|
return null;
|
||||||
@@ -47,25 +44,12 @@ final class Payload {
|
|||||||
$payload->nonce = $data->nonce;
|
$payload->nonce = $data->nonce;
|
||||||
$payload->json = ($data->json ?? true);
|
$payload->json = ($data->json ?? true);
|
||||||
$payload->scope = Scope::tryFrom($data->scope ?? '') ?? Scope::Cookie;
|
$payload->scope = Scope::tryFrom($data->scope ?? '') ?? Scope::Cookie;
|
||||||
|
$payload->token = $data->token;
|
||||||
/* we accept either a token or a password, not both */
|
|
||||||
if (strlen($data->token ?? '') > 0) {
|
|
||||||
$payload->token = $data->token;
|
|
||||||
$payload->password = null;
|
|
||||||
} else {
|
|
||||||
$payload->token = null;
|
|
||||||
$payload->password = $data->password;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Payload::constrict($payload);
|
return Payload::constrict($payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function constrict(Payload $payload): Payload {
|
public static function constrict(Payload $payload): Payload {
|
||||||
/* When using password, scope will be considered None. */
|
|
||||||
if ($payload->password) {
|
|
||||||
$payload->scope = Scope::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When scope is None, json will be considered false. */
|
/* When scope is None, json will be considered false. */
|
||||||
if ($payload->scope === Scope::None) {
|
if ($payload->scope === Scope::None) {
|
||||||
$payload->json = false;
|
$payload->json = false;
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ declare(strict_types=1);
|
|||||||
namespace App\Listener;
|
namespace App\Listener;
|
||||||
|
|
||||||
use App\Trait\CookieNameTrait;
|
use App\Trait\CookieNameTrait;
|
||||||
|
use App\Trait\HasLoggerTrait;
|
||||||
use App\Trait\StringTrait;
|
use App\Trait\StringTrait;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Cache\InvalidArgumentException;
|
use Psr\Cache\InvalidArgumentException;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
|
|
||||||
final readonly class AcceptListener {
|
final readonly class AcceptListener {
|
||||||
use CookieNameTrait;
|
use CookieNameTrait;
|
||||||
|
use HasLoggerTrait;
|
||||||
use StringTrait;
|
use StringTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CacheItemPoolInterface $sessionPool,
|
private CacheItemPoolInterface $sessionCache,
|
||||||
private LoggerInterface $logger,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
@@ -28,9 +28,9 @@ final readonly class AcceptListener {
|
|||||||
if ($event->getRequest()->cookies->has($this->cookieName())) {
|
if ($event->getRequest()->cookies->has($this->cookieName())) {
|
||||||
$cookie = $event->getRequest()->cookies->get($this->cookieName());
|
$cookie = $event->getRequest()->cookies->get($this->cookieName());
|
||||||
$cookieKey = $this->makeCacheKey("cookie_$cookie");
|
$cookieKey = $this->makeCacheKey("cookie_$cookie");
|
||||||
if ($this->sessionPool->hasItem($cookieKey)) {
|
if ($this->sessionCache->hasItem($cookieKey)) {
|
||||||
/* cookie sent corresponds to valid existing session */
|
/* cookie sent corresponds to valid existing session */
|
||||||
$id = $this->sessionPool->getItem($cookieKey)->get();
|
$id = $this->sessionCache->getItem($cookieKey)->get();
|
||||||
$this->logger->debug("has valid cookie-session: $id");
|
$this->logger->debug("has valid cookie-session: $id");
|
||||||
$event->setResponse(new Response("hi $id",
|
$event->setResponse(new Response("hi $id",
|
||||||
headers: ['Content-Type' => 'text/plain']
|
headers: ['Content-Type' => 'text/plain']
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ declare(strict_types=1);
|
|||||||
namespace App\Listener;
|
namespace App\Listener;
|
||||||
|
|
||||||
use App\ConfigBag;
|
use App\ConfigBag;
|
||||||
|
use App\Trait\HasLoggerTrait;
|
||||||
use App\Trait\StringTrait;
|
use App\Trait\StringTrait;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Cache\InvalidArgumentException;
|
use Psr\Cache\InvalidArgumentException;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
|
|
||||||
final readonly class AllowListener {
|
final readonly class AllowListener {
|
||||||
|
use HasLoggerTrait;
|
||||||
use StringTrait;
|
use StringTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CacheItemPoolInterface $sessionPool,
|
private CacheItemPoolInterface $sessionCache,
|
||||||
private ConfigBag $config,
|
private ConfigBag $config,
|
||||||
private LoggerInterface $logger,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
@@ -26,9 +26,9 @@ final readonly class AllowListener {
|
|||||||
public function onKernelRequest(RequestEvent $event): void {
|
public function onKernelRequest(RequestEvent $event): void {
|
||||||
if ($this->config->ipTtl() > 0) {
|
if ($this->config->ipTtl() > 0) {
|
||||||
$ipKey = $this->makeCacheKey("ip_{$event->getRequest()->getClientIp()}");
|
$ipKey = $this->makeCacheKey("ip_{$event->getRequest()->getClientIp()}");
|
||||||
if ($this->sessionPool->hasItem($ipKey)) {
|
if ($this->sessionCache->hasItem($ipKey)) {
|
||||||
/* ip address corresponds to valid existing session */
|
/* ip address corresponds to valid existing session */
|
||||||
$id = $this->sessionPool->getItem($ipKey)->get();
|
$id = $this->sessionCache->getItem($ipKey)->get();
|
||||||
$this->logger->debug("has valid ip-session: $id");
|
$this->logger->debug("has valid ip-session: $id");
|
||||||
$event->setResponse(new Response("hi $id",
|
$event->setResponse(new Response("hi $id",
|
||||||
headers: ['Content-Type' => 'text/plain']
|
headers: ['Content-Type' => 'text/plain']
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
namespace App\Listener;
|
namespace App\Listener;
|
||||||
|
|
||||||
use App\ConfigBag;
|
use App\ConfigBag;
|
||||||
|
use App\Trait\HasLoggerTrait;
|
||||||
use App\Trait\MakeNonceTrait;
|
use App\Trait\MakeNonceTrait;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
|
||||||
use Psr\Cache\InvalidArgumentException;
|
use Psr\Cache\InvalidArgumentException;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
@@ -17,18 +16,13 @@ use Twig\Error\RuntimeError;
|
|||||||
use Twig\Error\SyntaxError;
|
use Twig\Error\SyntaxError;
|
||||||
|
|
||||||
final readonly class InterceptListener {
|
final readonly class InterceptListener {
|
||||||
|
use HasLoggerTrait;
|
||||||
use MakeNonceTrait;
|
use MakeNonceTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CacheItemPoolInterface $requestPool,
|
private ConfigBag $config,
|
||||||
private ConfigBag $config,
|
private Environment $twig,
|
||||||
private Environment $twig,
|
) {}
|
||||||
CacheItemPoolInterface $noncePool,
|
|
||||||
LoggerInterface $logger,
|
|
||||||
) {
|
|
||||||
$this->logger = $logger;
|
|
||||||
$this->noncePool = $noncePool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws InvalidArgumentException|RuntimeError|SyntaxError|LoaderError */
|
/** @throws InvalidArgumentException|RuntimeError|SyntaxError|LoaderError */
|
||||||
#[AsEventListener(priority: 55)]
|
#[AsEventListener(priority: 55)]
|
||||||
|
|||||||
@@ -3,23 +3,24 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Listener;
|
namespace App\Listener;
|
||||||
|
|
||||||
use App\ConfigBag;
|
|
||||||
use App\Data\Payload;
|
use App\Data\Payload;
|
||||||
use App\Enum\Scope;
|
use App\Enum\Scope;
|
||||||
use App\MonitorCacheKeys;
|
use App\MonitorCacheKeys;
|
||||||
use App\Trait\CookieNameTrait;
|
use App\Trait\CookieNameTrait;
|
||||||
use App\Trait\GetTotpTrait;
|
use App\Trait\GetTotpTrait;
|
||||||
|
use App\Trait\HasLoggerTrait;
|
||||||
use App\Trait\MakeNonceTrait;
|
use App\Trait\MakeNonceTrait;
|
||||||
use App\Trait\StringTrait;
|
use App\Trait\StringTrait;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Cache\InvalidArgumentException;
|
use Psr\Cache\InvalidArgumentException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||||
use Symfony\Component\HttpFoundation\Cookie;
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
|
||||||
use Symfony\Component\Uid\Ulid;
|
use Symfony\Component\Uid\Ulid;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Error\LoaderError;
|
use Twig\Error\LoaderError;
|
||||||
@@ -28,27 +29,22 @@ use Twig\Error\SyntaxError;
|
|||||||
|
|
||||||
final readonly class LoginListener {
|
final readonly class LoginListener {
|
||||||
use CookieNameTrait;
|
use CookieNameTrait;
|
||||||
|
use HasLoggerTrait;
|
||||||
use MakeNonceTrait;
|
use MakeNonceTrait;
|
||||||
use StringTrait;
|
use StringTrait;
|
||||||
use GetTotpTrait;
|
use GetTotpTrait;
|
||||||
|
|
||||||
private CacheItemPoolInterface $requestPool;
|
private CacheItemPoolInterface $sessionCache;
|
||||||
private CacheItemPoolInterface $sessionPool;
|
private RateLimiterFactoryInterface $rateLimiter;
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ConfigBag $config,
|
private Environment $twig,
|
||||||
private Environment $twig,
|
CacheItemPoolInterface $sessionCache,
|
||||||
CacheItemPoolInterface $noncePool,
|
#[Target('login_limiter')] RateLimiterFactoryInterface $rateLimiter,
|
||||||
CacheItemPoolInterface $requestPool,
|
|
||||||
CacheItemPoolInterface $sessionPool,
|
|
||||||
LoggerInterface $logger,
|
|
||||||
) {
|
) {
|
||||||
$this->config = $config;
|
$this->sessionCache = new MonitorCacheKeys($sessionCache);
|
||||||
$this->requestPool = new MonitorCacheKeys($requestPool);
|
$this->rateLimiter = $rateLimiter;
|
||||||
$this->sessionPool = new MonitorCacheKeys($sessionPool);
|
|
||||||
$this->noncePool = $noncePool;
|
|
||||||
$this->logger = $logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException|LoaderError|RuntimeError|SyntaxError */
|
/** @throws InvalidArgumentException|LoaderError|RuntimeError|SyntaxError */
|
||||||
@@ -58,13 +54,8 @@ final readonly class LoginListener {
|
|||||||
$data = $event->getRequest()->headers->get($this->headerName());
|
$data = $event->getRequest()->headers->get($this->headerName());
|
||||||
$payload = Payload::decode($data);
|
$payload = Payload::decode($data);
|
||||||
$response = null;
|
$response = null;
|
||||||
if ($payload) {
|
if ($payload && $payload->token) {
|
||||||
/* if using token */
|
$response = $this->checkToken($payload, $event->getRequest());
|
||||||
if ($payload->token) {
|
|
||||||
$response = $this->checkToken($payload, $event->getRequest());
|
|
||||||
} else if ($this->config->staticSecret()) {
|
|
||||||
$response = $this->checkPassword($payload);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* token or password authentication was successful */
|
/* token or password authentication was successful */
|
||||||
@@ -73,10 +64,7 @@ final readonly class LoginListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$limitReached = $this->logFailure(
|
$limitReached = $this->logFailure($event->getRequest());
|
||||||
$payload ? $payload->toString() : $data,
|
|
||||||
$event->getRequest()
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->logger->debug("logging failure for: {$event->getRequest()->getClientIp()}");
|
$this->logger->debug("logging failure for: {$event->getRequest()->getClientIp()}");
|
||||||
$event->setResponse($this->makeFailedResponse($limitReached, $payload->json ?? true));
|
$event->setResponse($this->makeFailedResponse($limitReached, $payload->json ?? true));
|
||||||
@@ -95,12 +83,12 @@ final readonly class LoginListener {
|
|||||||
/* token is correct */
|
/* token is correct */
|
||||||
|
|
||||||
/* if server nonce is found and is valid */
|
/* if server nonce is found and is valid */
|
||||||
$nonceItem = $this->noncePool->getItem($payload->nonce);
|
$nonceItem = $this->nonceCache->getItem($payload->nonce);
|
||||||
if ($nonceItem->isHit() && $nonceItem->get()) {
|
if ($nonceItem->isHit() && $nonceItem->get()) {
|
||||||
/* mark nonce as spent */
|
/* mark nonce as spent */
|
||||||
$nonceItem->set(false); /* invalid */
|
$nonceItem->set(false); /* invalid */
|
||||||
$nonceItem->expiresAfter(LoginListener::NONCE_TTL); /* keep briefly */
|
$nonceItem->expiresAfter(LoginListener::NONCE_TTL); /* keep briefly */
|
||||||
$this->noncePool->save($nonceItem);
|
$this->nonceCache->save($nonceItem);
|
||||||
|
|
||||||
/* token authentication successful, grant access and set response */
|
/* token authentication successful, grant access and set response */
|
||||||
$cleanId = $this->makeCacheKey($payload->id);
|
$cleanId = $this->makeCacheKey($payload->id);
|
||||||
@@ -144,42 +132,11 @@ final readonly class LoginListener {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
|
||||||
private function checkPassword(Payload $payload): ?Response {
|
|
||||||
/* When using password but password is disabled, request will always fail. */
|
|
||||||
|
|
||||||
/* if password is correct */
|
|
||||||
|
|
||||||
if (hash_equals($this->config->staticSecret(), $payload->password)) {
|
|
||||||
/* password is correct */
|
|
||||||
|
|
||||||
/* nonce *may* be client provided, but must still be unique */
|
|
||||||
|
|
||||||
/* if server/client nonce is acceptable (valid server or unused client) */
|
|
||||||
$nonceItem = $this->noncePool->getItem($payload->nonce);
|
|
||||||
if (($nonceItem->isHit() && $nonceItem->get()) || ! $nonceItem->isHit()) {
|
|
||||||
/* mark nonce as spent */
|
|
||||||
$nonceItem->set(false); /* invalid */
|
|
||||||
$nonceItem->expiresAfter(LoginListener::NONCE_TTL); /* keep briefly */
|
|
||||||
$this->noncePool->save($nonceItem);
|
|
||||||
|
|
||||||
/* password authentication successful, grant access and set response */
|
|
||||||
$cleanId = $this->makeCacheKey($payload->id);
|
|
||||||
$this->logger->debug("successful login for: $cleanId");
|
|
||||||
return new Response("hi $cleanId",
|
|
||||||
Response::HTTP_OK,
|
|
||||||
['Content-Type' => 'text/plain']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
private function setCookie(string $id): Cookie {
|
private function setCookie(string $id): Cookie {
|
||||||
/* successful auth with token, store session and set the cookie */
|
/* successful auth with token, store session and set the cookie */
|
||||||
$ulid = new Ulid();
|
$ulid = new Ulid();
|
||||||
$sessionCookie = $this->sessionPool->getItem(
|
$sessionCookie = $this->sessionCache->getItem(
|
||||||
$this->makeCacheKey("cookie_$ulid")
|
$this->makeCacheKey("cookie_$ulid")
|
||||||
);
|
);
|
||||||
if ($sessionCookie->isHit()) {
|
if ($sessionCookie->isHit()) {
|
||||||
@@ -189,7 +146,7 @@ final readonly class LoginListener {
|
|||||||
}
|
}
|
||||||
$sessionCookie->set($id);
|
$sessionCookie->set($id);
|
||||||
$sessionCookie->expiresAfter($this->config->cookieTtl());
|
$sessionCookie->expiresAfter($this->config->cookieTtl());
|
||||||
$this->sessionPool->save($sessionCookie);
|
$this->sessionCache->save($sessionCookie);
|
||||||
|
|
||||||
return Cookie::create(
|
return Cookie::create(
|
||||||
name: $this->cookieName(),
|
name: $this->cookieName(),
|
||||||
@@ -205,29 +162,15 @@ final readonly class LoginListener {
|
|||||||
/* successful auth with token, requested scope of ip (and ip access enabled) */
|
/* successful auth with token, requested scope of ip (and ip access enabled) */
|
||||||
$ipKey = $this->makeCacheKey("ip_$ip");
|
$ipKey = $this->makeCacheKey("ip_$ip");
|
||||||
|
|
||||||
$sessionIp = $this->sessionPool->getItem($ipKey);
|
$sessionIp = $this->sessionCache->getItem($ipKey);
|
||||||
$sessionIp->set($id);
|
$sessionIp->set($id);
|
||||||
$sessionIp->expiresAfter($this->config->ipTtl());
|
$sessionIp->expiresAfter($this->config->ipTtl());
|
||||||
$this->sessionPool->save($sessionIp);
|
$this->sessionCache->save($sessionIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
private function logFailure(Request $request): bool {
|
||||||
private function logFailure(string $data, Request $request): bool {
|
$limiter = $this->rateLimiter->create($request->getClientIp());
|
||||||
// TODO use rate-limiting symfony system (also update RejectListener)
|
return ($limiter->consume(1)->getRemainingTokens() < 1);
|
||||||
$timeframe = (int)floor(time() / $this->getTotp()->getPeriod());
|
|
||||||
/* hash the data and timeframe, so we do not count duplicates in the same timeframe
|
|
||||||
* hitting refresh a few times should not lock you out */
|
|
||||||
$ipKey = $this->makeCacheKey("ip_{$request->getClientIp()}");
|
|
||||||
$failuresItem = $this->requestPool->getItem($ipKey);
|
|
||||||
$failures = $failuresItem->get() ?? [];
|
|
||||||
$failures[hash('xxh3', "$timeframe-$data")] = true;
|
|
||||||
$limitReached = count($failures) >= $this->config->limit();
|
|
||||||
$failuresItem->set($failures);
|
|
||||||
$failuresItem->expiresAfter($limitReached
|
|
||||||
? $this->config->limitTtl() : $this->config->limitTimeout()
|
|
||||||
);
|
|
||||||
$this->requestPool->save($failuresItem);
|
|
||||||
return $limitReached;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException|RuntimeError|SyntaxError|LoaderError */
|
/** @throws InvalidArgumentException|RuntimeError|SyntaxError|LoaderError */
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Listener;
|
|
||||||
|
|
||||||
use App\ConfigBag;
|
|
||||||
use App\Trait\GetTotpTrait;
|
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
|
||||||
|
|
||||||
final readonly class LookupListener {
|
|
||||||
use GetTotpTrait;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
ConfigBag $config,
|
|
||||||
) {
|
|
||||||
$this->config = $config;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[AsEventListener(priority: 44)]
|
|
||||||
public function onKernelResponse(ResponseEvent $event): void {
|
|
||||||
/* if you enable a static secret, you may choose to also enable
|
|
||||||
* a totp-lookup, which contains a special path, when successfully
|
|
||||||
* authenticated, instead of returning "hi <your-name>" it returns
|
|
||||||
* "code <totp-code>" */
|
|
||||||
if ($this->config->staticSecret() && $this->config->lookupTotp()) {
|
|
||||||
if (str_contains($event->getRequest()->getPathInfo(),
|
|
||||||
$this->config->lookupTotp()) &&
|
|
||||||
$event->getResponse()->getStatusCode() === Response::HTTP_OK
|
|
||||||
) {
|
|
||||||
$totp = $this->getTotp();
|
|
||||||
$next = $totp->at(time() + $totp->getPeriod());
|
|
||||||
$event->setResponse(new Response(
|
|
||||||
"next {$next}",
|
|
||||||
Response::HTTP_OK,
|
|
||||||
['Content-Type' => 'text/plain']
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,45 +4,44 @@ declare(strict_types=1);
|
|||||||
namespace App\Listener;
|
namespace App\Listener;
|
||||||
|
|
||||||
use App\ConfigBag;
|
use App\ConfigBag;
|
||||||
|
use App\Trait\HasLoggerTrait;
|
||||||
use App\Trait\StringTrait;
|
use App\Trait\StringTrait;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||||
use Psr\Cache\InvalidArgumentException;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Error\LoaderError;
|
use Twig\Error\LoaderError;
|
||||||
use Twig\Error\RuntimeError;
|
use Twig\Error\RuntimeError;
|
||||||
use Twig\Error\SyntaxError;
|
use Twig\Error\SyntaxError;
|
||||||
|
|
||||||
final readonly class RejectListener {
|
final readonly class RejectListener {
|
||||||
|
use HasLoggerTrait;
|
||||||
use StringTrait;
|
use StringTrait;
|
||||||
|
|
||||||
public function __construct(
|
private RateLimiterFactoryInterface $rateLimiter;
|
||||||
private CacheItemPoolInterface $requestPool,
|
|
||||||
private ConfigBag $config,
|
|
||||||
private Environment $twig,
|
|
||||||
private LoggerInterface $logger,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/** @throws SyntaxError|InvalidArgumentException|RuntimeError|LoaderError */
|
public function __construct(
|
||||||
|
private ConfigBag $config,
|
||||||
|
private Environment $twig,
|
||||||
|
#[Target('login_limiter')] RateLimiterFactoryInterface $rateLimiter,
|
||||||
|
) {
|
||||||
|
$this->rateLimiter = $rateLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @throws SyntaxError|RuntimeError|LoaderError */
|
||||||
#[AsEventListener(priority: 77)]
|
#[AsEventListener(priority: 77)]
|
||||||
public function onKernelRequest(RequestEvent $event): void {
|
public function onKernelRequest(RequestEvent $event): void {
|
||||||
$ipKey = $this->makeCacheKey("ip_{$event->getRequest()->getClientIp()}");
|
|
||||||
|
|
||||||
/* check if they have made too many failed login attempts */
|
/* check if they have made too many failed login attempts */
|
||||||
$failuresItem = $this->requestPool->getItem($ipKey);
|
$limiter = $this->rateLimiter->create($event->getRequest()->getClientIp());
|
||||||
if ($failuresItem->isHit()) {
|
if ($limiter->consume(0)->getRemainingTokens() < 1) {
|
||||||
$failures = $failuresItem->get() ?? [];
|
$this->logger->debug("already blocked: {$event->getRequest()->getClientIp()}");
|
||||||
if (count($failures) >= $this->config->limit()) {
|
$html = $this->twig->render('error.html.twig');
|
||||||
$this->logger->debug("already blocked: {$event->getRequest()->getClientIp()}");
|
$event->setResponse(new Response($html, ($this->config->teapot()
|
||||||
$html = $this->twig->render('error.html.twig');
|
? Response::HTTP_I_AM_A_TEAPOT : Response::HTTP_TOO_MANY_REQUESTS),
|
||||||
$event->setResponse(new Response($html, ($this->config->teapot()
|
['Content-Type' => 'text/html']
|
||||||
? Response::HTTP_I_AM_A_TEAPOT : Response::HTTP_TOO_MANY_REQUESTS),
|
));
|
||||||
['Content-Type' => 'text/html']
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-47
@@ -12,14 +12,16 @@ use Psr\Cache\InvalidArgumentException;
|
|||||||
* because it can change from outside this object instance. */
|
* because it can change from outside this object instance. */
|
||||||
final readonly class MonitorCacheKeys implements CacheItemPoolInterface {
|
final readonly class MonitorCacheKeys implements CacheItemPoolInterface {
|
||||||
private const KEY_LIST = '__key_list';
|
private const KEY_LIST = '__key_list';
|
||||||
private const IS_DIRTY = '__is_dirty';
|
private const CHANGE_LIST = '__chg_list';
|
||||||
|
public const UPDATED = 1;
|
||||||
|
public const REMOVED = 2;
|
||||||
|
|
||||||
private CacheItemPoolInterface $cache;
|
private CacheItemPoolInterface $cache;
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function __construct(CacheItemPoolInterface $cache) {
|
public function __construct(CacheItemPoolInterface $cache) {
|
||||||
$this->cache = $cache;
|
$this->cache = $cache;
|
||||||
$items = $cache->getItems([self::KEY_LIST, self::IS_DIRTY]);
|
$items = $cache->getItems([self::KEY_LIST, self::CHANGE_LIST]);
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if ( ! $item->isHit()) {
|
if ( ! $item->isHit()) {
|
||||||
$this->initialize();
|
$this->initialize();
|
||||||
@@ -31,11 +33,11 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface {
|
|||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
private function initialize(): void {
|
private function initialize(): void {
|
||||||
$keyList = $this->cache->getItem(self::KEY_LIST);
|
$keyList = $this->cache->getItem(self::KEY_LIST);
|
||||||
$isDirty = $this->cache->getItem(self::IS_DIRTY);
|
$changeList = $this->cache->getItem(self::CHANGE_LIST);
|
||||||
$keyList->set([]);
|
$keyList->set([]);
|
||||||
$isDirty->set(false);
|
$changeList->set([]);
|
||||||
$this->cache->saveDeferred($keyList);
|
$this->cache->saveDeferred($keyList);
|
||||||
$this->cache->saveDeferred($isDirty);
|
$this->cache->saveDeferred($changeList);
|
||||||
$this->cache->commit();
|
$this->cache->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,22 +48,24 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function isDirty(): bool {
|
public function getChanges(): array {
|
||||||
$isDirty = $this->cache->getItem(self::IS_DIRTY);
|
$changeList = $this->cache->getItem(self::CHANGE_LIST);
|
||||||
return $isDirty->get() ?? false;
|
return $changeList->get() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function markClean(): void {
|
public function markClean(): void {
|
||||||
$isDirty = $this->cache->getItem(self::IS_DIRTY);
|
$changeList = $this->cache->getItem(self::CHANGE_LIST);
|
||||||
$isDirty->set(false);
|
$changeList->set([]);
|
||||||
$this->cache->save($isDirty);
|
$this->cache->save($changeList);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getItem(string $key): CacheItemInterface {
|
public function getItem(string $key): CacheItemInterface {
|
||||||
return $this->cache->getItem($key);
|
return $this->cache->getItem($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return CacheItemInterface[]
|
||||||
|
* @throws InvalidArgumentException */
|
||||||
public function getItems(array $keys = []): iterable {
|
public function getItems(array $keys = []): iterable {
|
||||||
return $this->cache->getItems($keys);
|
return $this->cache->getItems($keys);
|
||||||
}
|
}
|
||||||
@@ -83,20 +87,14 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function deleteItem(string $key): bool {
|
public function deleteItem(string $key): bool {
|
||||||
if ($key === self::KEY_LIST || $key === self::IS_DIRTY) {
|
$this->isValid($key);
|
||||||
throw new OutOfBoundsException(
|
|
||||||
'Can not delete the private key list or is dirty flag'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$keyList = $this->cache->getItem(self::KEY_LIST);
|
$keyList = $this->cache->getItem(self::KEY_LIST);
|
||||||
$isDirty = $this->cache->getItem(self::IS_DIRTY);
|
|
||||||
$keyValues = $keyList->get();
|
$keyValues = $keyList->get();
|
||||||
if (isset($keyValues[$key])) {
|
if (isset($keyValues[$key])) {
|
||||||
unset($keyValues[$key]);
|
unset($keyValues[$key]);
|
||||||
$keyList->set($keyValues);
|
$keyList->set($keyValues);
|
||||||
$isDirty->set(true);
|
|
||||||
$this->cache->saveDeferred($keyList);
|
$this->cache->saveDeferred($keyList);
|
||||||
$this->cache->saveDeferred($isDirty);
|
$this->logChange($key, MonitorCacheKeys::REMOVED);
|
||||||
$this->cache->commit();
|
$this->cache->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,25 +102,17 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function deleteItems(array $keys): bool {
|
public function deleteItems(array $keys): bool {
|
||||||
if (in_array(self::KEY_LIST, $keys, true) ||
|
$this->allValid($keys);
|
||||||
in_array(self::IS_DIRTY, $keys, true)
|
|
||||||
) {
|
|
||||||
throw new OutOfBoundsException(
|
|
||||||
'Can not delete the private key list or is dirty flag'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$keyList = $this->cache->getItem(self::KEY_LIST);
|
$keyList = $this->cache->getItem(self::KEY_LIST);
|
||||||
$isDirty = $this->cache->getItem(self::IS_DIRTY);
|
|
||||||
$keyValues = $keyList->get();
|
$keyValues = $keyList->get();
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
if (isset($keyValues[$key])) {
|
if (isset($keyValues[$key])) {
|
||||||
unset($keyValues[$key]);
|
unset($keyValues[$key]);
|
||||||
$isDirty->set(true);
|
$this->logChange($key, MonitorCacheKeys::REMOVED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$keyList->set($keyValues);
|
$keyList->set($keyValues);
|
||||||
$this->cache->saveDeferred($keyList);
|
$this->cache->saveDeferred($keyList);
|
||||||
$this->cache->saveDeferred($isDirty);
|
|
||||||
$this->cache->commit();
|
$this->cache->commit();
|
||||||
|
|
||||||
return $this->cache->deleteItems($keys);
|
return $this->cache->deleteItems($keys);
|
||||||
@@ -140,25 +130,48 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface {
|
|||||||
return $this->cache->saveDeferred($item);
|
return $this->cache->saveDeferred($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
|
||||||
private function update(CacheItemInterface $item) {
|
|
||||||
if ($item->getKey() === self::KEY_LIST || $item->getKey() === self::IS_DIRTY) {
|
|
||||||
throw new OutOfBoundsException(
|
|
||||||
'Can not alter the private key list or is dirty flag'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$keyList = $this->cache->getItem(self::KEY_LIST);
|
|
||||||
$isDirty = $this->cache->getItem(self::IS_DIRTY);
|
|
||||||
$keyValues = $keyList->get();
|
|
||||||
$keyValues[$item->getKey()] = true;
|
|
||||||
$keyList->set($keyValues);
|
|
||||||
$isDirty->set(true);
|
|
||||||
$this->cache->saveDeferred($keyList);
|
|
||||||
$this->cache->saveDeferred($isDirty);
|
|
||||||
$this->cache->commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function commit(): bool {
|
public function commit(): bool {
|
||||||
return $this->cache->commit();
|
return $this->cache->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @throws InvalidArgumentException|OutOfBoundsException */
|
||||||
|
private function update(CacheItemInterface $item) {
|
||||||
|
$this->isValid($item->getKey());
|
||||||
|
$keyList = $this->cache->getItem(self::KEY_LIST);
|
||||||
|
$keyValues = $keyList->get();
|
||||||
|
$keyValues[$item->getKey()] = true;
|
||||||
|
$keyList->set($keyValues);
|
||||||
|
$this->logChange($item->getKey());
|
||||||
|
$this->cache->saveDeferred($keyList);
|
||||||
|
$this->cache->commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @throws OutOfBoundsException */
|
||||||
|
private function isValid(string $key): void {
|
||||||
|
if ($key === self::KEY_LIST || $key === self::CHANGE_LIST) {
|
||||||
|
throw new OutOfBoundsException(
|
||||||
|
'Can not modify the private key or change lists'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @throws OutOfBoundsException */
|
||||||
|
private function allValid(array $keys): void {
|
||||||
|
if (in_array(self::KEY_LIST, $keys, true) ||
|
||||||
|
in_array(self::CHANGE_LIST, $keys, true)
|
||||||
|
) {
|
||||||
|
throw new OutOfBoundsException(
|
||||||
|
'Can not modify the private key or change lists'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @throws InvalidArgumentException */
|
||||||
|
private function logChange(string $key, int $code = MonitorCacheKeys::UPDATED): void {
|
||||||
|
$changeList = $this->cache->getItem(self::CHANGE_LIST);
|
||||||
|
$changeValues = $changeList->get();
|
||||||
|
$changeValues[$key] = $code;
|
||||||
|
$changeList->set($changeValues);
|
||||||
|
$this->cache->saveDeferred($changeList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-43
@@ -9,67 +9,46 @@ use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
|||||||
|
|
||||||
#[Autoconfigure(public: true)]
|
#[Autoconfigure(public: true)]
|
||||||
final readonly class PersistCache {
|
final readonly class PersistCache {
|
||||||
private MonitorCacheKeys $requestPool;
|
private MonitorCacheKeys $sessionCache;
|
||||||
private MonitorCacheKeys $persistRequestPool;
|
private MonitorCacheKeys $sessionStorage;
|
||||||
private MonitorCacheKeys $sessionPool;
|
|
||||||
private MonitorCacheKeys $persistSessionPool;
|
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function __construct(
|
public function __construct(
|
||||||
CacheItemPoolInterface $requestPool,
|
CacheItemPoolInterface $sessionCache,
|
||||||
CacheItemPoolInterface $persistRequestPool,
|
CacheItemPoolInterface $sessionStorage,
|
||||||
CacheItemPoolInterface $sessionPool,
|
|
||||||
CacheItemPoolInterface $persistSessionPool
|
|
||||||
) {
|
) {
|
||||||
$this->requestPool = new MonitorCacheKeys($requestPool);
|
$this->sessionCache = new MonitorCacheKeys($sessionCache);
|
||||||
$this->persistRequestPool = new MonitorCacheKeys($persistRequestPool);
|
$this->sessionStorage = new MonitorCacheKeys($sessionStorage);
|
||||||
$this->sessionPool = new MonitorCacheKeys($sessionPool);
|
|
||||||
$this->persistSessionPool = new MonitorCacheKeys($persistSessionPool);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function boot(): void {
|
public function boot(): void {
|
||||||
/* the caches are considered warm as soon as they are not empty */
|
/* the caches are considered warm as soon as they are not empty */
|
||||||
if (empty($this->requestPool->getKeys())) {
|
if (empty($this->sessionCache->getKeys())) {
|
||||||
$items = $this->persistRequestPool->getItems($this->persistRequestPool->getKeys());
|
$items = $this->sessionStorage->getItems($this->sessionStorage->getKeys());
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$this->requestPool->saveDeferred($item);
|
$this->sessionCache->saveDeferred($item);
|
||||||
}
|
}
|
||||||
$this->requestPool->markClean();
|
$this->sessionCache->markClean();
|
||||||
$this->requestPool->commit();
|
$this->sessionCache->commit();
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($this->sessionPool->getKeys())) {
|
|
||||||
$items = $this->persistSessionPool->getItems($this->persistSessionPool->getKeys());
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$this->sessionPool->saveDeferred($item);
|
|
||||||
}
|
|
||||||
$this->sessionPool->markClean();
|
|
||||||
$this->sessionPool->commit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function persist(): void {
|
public function persist(): void {
|
||||||
/* we only need to persist the caches if they contain changes */
|
/* we only need to persist the changes made to the cache (if any) */
|
||||||
if ($this->requestPool->isDirty()) {
|
$changes = $this->sessionCache->getChanges();
|
||||||
$this->requestPool->markClean();
|
if ($changes) {
|
||||||
$items = $this->requestPool->getItems($this->requestPool->getKeys());
|
$this->sessionCache->markClean();
|
||||||
$this->persistRequestPool->clear();
|
$items = $this->sessionCache->getItems(array_keys($changes));
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$this->persistRequestPool->saveDeferred($item);
|
if (($changes[$item->getKey()] ?? null) === MonitorCacheKeys::REMOVED) {
|
||||||
|
$this->sessionStorage->deleteItem($item->getKey());
|
||||||
|
} else {
|
||||||
|
$this->sessionStorage->saveDeferred($item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$this->persistRequestPool->commit();
|
$this->sessionStorage->commit();
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->sessionPool->isDirty()) {
|
|
||||||
$this->sessionPool->markClean();
|
|
||||||
$items = $this->sessionPool->getItems($this->sessionPool->getKeys());
|
|
||||||
$this->persistSessionPool->clear();
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$this->persistSessionPool->saveDeferred($item);
|
|
||||||
}
|
|
||||||
$this->persistSessionPool->commit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ use App\ConfigBag;
|
|||||||
use OTPHP\Factory;
|
use OTPHP\Factory;
|
||||||
use OTPHP\TOTPInterface;
|
use OTPHP\TOTPInterface;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
use Symfony\Contracts\Service\Attribute\Required;
|
||||||
|
|
||||||
trait GetTotpTrait {
|
trait GetTotpTrait {
|
||||||
protected readonly ConfigBag $config;
|
protected readonly ConfigBag $config;
|
||||||
|
|
||||||
|
#[Required]
|
||||||
|
public function setConfig(ConfigBag $config): void {
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
protected function getTotp(): TOTPInterface {
|
protected function getTotp(): TOTPInterface {
|
||||||
$otp = Factory::loadFromProvisioningUri(
|
$otp = Factory::loadFromProvisioningUri(
|
||||||
$this->config->totpUri(), $this->config->clock()
|
$this->config->totpUri(), $this->config->clock()
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Trait;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Contracts\Service\Attribute\Required;
|
||||||
|
|
||||||
|
trait HasLoggerTrait {
|
||||||
|
protected readonly LoggerInterface $logger;
|
||||||
|
|
||||||
|
#[Required]
|
||||||
|
public function setLogger(LoggerInterface $logger): void {
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,17 +6,23 @@ namespace App\Trait;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Cache\InvalidArgumentException;
|
use Psr\Cache\InvalidArgumentException;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
use Symfony\Contracts\Service\Attribute\Required;
|
||||||
|
|
||||||
trait MakeNonceTrait {
|
trait MakeNonceTrait {
|
||||||
|
use HasLoggerTrait;
|
||||||
|
|
||||||
/* 15 bytes neatly fits in base64 */
|
/* 15 bytes neatly fits in base64 */
|
||||||
private const NONCE_LENGTH = 15;
|
private const NONCE_LENGTH = 15;
|
||||||
private const NONCE_TTL = 120;
|
private const NONCE_TTL = 120;
|
||||||
|
|
||||||
protected readonly CacheItemPoolInterface $noncePool;
|
protected readonly CacheItemPoolInterface $nonceCache;
|
||||||
protected readonly LoggerInterface $logger;
|
|
||||||
|
#[Required]
|
||||||
|
public function setNonceCache(CacheItemPoolInterface $nonceCache): void {
|
||||||
|
$this->nonceCache = $nonceCache;
|
||||||
|
}
|
||||||
|
|
||||||
/** @throws InvalidArgumentException|Exception */
|
/** @throws InvalidArgumentException|Exception */
|
||||||
protected function makeNonce(int $retries = 3): string {
|
protected function makeNonce(int $retries = 3): string {
|
||||||
@@ -24,7 +30,7 @@ trait MakeNonceTrait {
|
|||||||
$nonce = rtrim(strtr(base64_encode(random_bytes(
|
$nonce = rtrim(strtr(base64_encode(random_bytes(
|
||||||
static::NONCE_LENGTH
|
static::NONCE_LENGTH
|
||||||
)), '+/', '-_'), '=');
|
)), '+/', '-_'), '=');
|
||||||
$nonceItem = $this->noncePool->getItem($nonce);
|
$nonceItem = $this->nonceCache->getItem($nonce);
|
||||||
|
|
||||||
if ($nonceItem->isHit()) {
|
if ($nonceItem->isHit()) {
|
||||||
if ($retries < 1) {
|
if ($retries < 1) {
|
||||||
@@ -41,7 +47,7 @@ trait MakeNonceTrait {
|
|||||||
$nonceItem->set(true); /* valid */
|
$nonceItem->set(true); /* valid */
|
||||||
$nonceItem->expiresAfter(static::NONCE_TTL);
|
$nonceItem->expiresAfter(static::NONCE_TTL);
|
||||||
$this->logger->debug("added nonce: $nonce");
|
$this->logger->debug("added nonce: $nonce");
|
||||||
$this->noncePool->save($nonceItem);
|
$this->nonceCache->save($nonceItem);
|
||||||
return $nonce;
|
return $nonce;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
0
|
|
||||||
__key_list
|
|
||||||
a:0:{}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
0
|
|
||||||
__is_dirty
|
|
||||||
b:0;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
0
|
|
||||||
__key_list
|
|
||||||
a:0:{}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
0
|
|
||||||
__is_dirty
|
|
||||||
b:0;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
1766103213
|
|
||||||
suppress
|
|
||||||
N;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
32503594112
|
|
||||||
totp
|
|
||||||
s:138:"otpauth://totp/Preauth-TOTP?secret=5RL5FOJGV4XRKGT74ZVN4725OAM244SU7JYXYX4SHQDTJI4P3YKBYAFUVBBOCLI5XSOLERNB6IQQ54SIGY6QHJ26JM4OP3ZJVBUUBIY";
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script>
|
||||||
|
const form = document.getElementById('preauth-form');
|
||||||
|
const message = document.getElementById('preauth-message');
|
||||||
|
|
||||||
|
form.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
/* make base64url string containing our payload json object */
|
||||||
|
const data = btoa(JSON.stringify({
|
||||||
|
id: form.preauth_id.value,
|
||||||
|
token: form.preauth_token.value,
|
||||||
|
nonce: form.preauth_nonce.value,
|
||||||
|
json: true
|
||||||
|
})).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
|
||||||
|
/* send our request to the server */
|
||||||
|
fetch(window.location.href, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'X-Preauth': data },
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.headers.has('Location')) {
|
||||||
|
/* follow redirect (not needed in most browsers) */
|
||||||
|
window.location.href = response.headers.get('Location');
|
||||||
|
} else if (response.headers.get('Content-Type') === 'application/json') {
|
||||||
|
/* got json, update the page */
|
||||||
|
response.json().then((content) => {
|
||||||
|
if (Object.hasOwn(content, 'message')) {
|
||||||
|
message.innerText = content.message;
|
||||||
|
}
|
||||||
|
if (Object.hasOwn(content, 'nonce')) {
|
||||||
|
form.preauth_nonce.value = content.nonce;
|
||||||
|
form.preauth_token.value = '';
|
||||||
|
form.preauth_token.focus();
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log('failed to parse json from response');
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
} else { /* non-json, non-redirect response */
|
||||||
|
/* overwrite the page */
|
||||||
|
response.text().then((text) => {
|
||||||
|
document.open();
|
||||||
|
document.write(text);
|
||||||
|
document.close();
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log('failed to get text from response');
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -8,5 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
{{ include('_script.html.twig') }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,118 +8,9 @@
|
|||||||
<div class="right"><label for="preauth-id">{{ env.id_name }}:</label></div>
|
<div class="right"><label for="preauth-id">{{ env.id_name }}:</label></div>
|
||||||
<div><input type="text" name="preauth_id" id="preauth-id"
|
<div><input type="text" name="preauth_id" id="preauth-id"
|
||||||
autocomplete="username" required="required" autofocus="autofocus"></div>
|
autocomplete="username" required="required" autofocus="autofocus"></div>
|
||||||
{% if env.allow_password %}
|
|
||||||
<div class="right boxTk">
|
|
||||||
<label for="preauth-token">{{ env.token_name }}:</label><br>
|
|
||||||
<span id="preauth-use-pw">🔃 {{ env.password_name }}</span></div>
|
|
||||||
<div class="boxTk"><input type="text" name="preauth_token" id="preauth-token"
|
|
||||||
autocomplete="one-time-code" required="required"></div>
|
|
||||||
|
|
||||||
<div class="right boxPw hidden">
|
|
||||||
<label for="preauth-password">{{ env.password_name }}:</label><br>
|
|
||||||
<span id="preauth-use-tk">🔃 {{ env.token_name }}</span></div>
|
|
||||||
<div class="boxPw hidden"><input type="password" name="preauth_password" id="preauth-password"
|
|
||||||
autocomplete="password" disabled="disabled" required="required"></div>
|
|
||||||
{% else %}
|
|
||||||
<div class="right"><label for="preauth-token">{{ env.token_name }}:</label></div>
|
<div class="right"><label for="preauth-token">{{ env.token_name }}:</label></div>
|
||||||
<div><input type="text" name="preauth_token" id="preauth-token"
|
<div><input type="text" name="preauth_token" id="preauth-token"
|
||||||
autocomplete="one-time-code" required="required"></div>
|
autocomplete="one-time-code" required="required"></div>
|
||||||
{% endif %}
|
|
||||||
<div class="center"><button type="submit">{{ env.submit_name }}</button></div>
|
<div class="center"><button type="submit">{{ env.submit_name }}</button></div>
|
||||||
</form>
|
</form>
|
||||||
<script>
|
|
||||||
const form = document.getElementById('preauth-form');
|
|
||||||
const message = document.getElementById('preauth-message');
|
|
||||||
|
|
||||||
{% if env.allow_password %}
|
|
||||||
const usePw = document.getElementById('preauth-use-pw');
|
|
||||||
const useTk = document.getElementById('preauth-use-tk');
|
|
||||||
const boxPw = document.querySelectorAll('.boxPw');
|
|
||||||
const boxTk = document.querySelectorAll('.boxTk');
|
|
||||||
|
|
||||||
usePw.addEventListener('click', (event) => {
|
|
||||||
form.preauth_token.value = '';
|
|
||||||
form.preauth_token.disabled = true;
|
|
||||||
form.preauth_password.disabled = false;
|
|
||||||
boxTk.forEach((element) => {
|
|
||||||
element.classList.add('hidden');
|
|
||||||
});
|
|
||||||
boxPw.forEach((element) => {
|
|
||||||
element.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useTk.addEventListener('click', (event) => {
|
|
||||||
form.preauth_password.value = '';
|
|
||||||
form.preauth_password.disabled = true;
|
|
||||||
form.preauth_token.disabled = false;
|
|
||||||
boxPw.forEach((element) => {
|
|
||||||
element.classList.add('hidden');
|
|
||||||
});
|
|
||||||
boxTk.forEach((element) => {
|
|
||||||
element.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
form.addEventListener('submit', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
{% if env.allow_password %}
|
|
||||||
/* make base64url string containing our payload json object */
|
|
||||||
/* payload will contain either token or password */
|
|
||||||
const data = btoa(JSON.stringify({
|
|
||||||
id: form.preauth_id.value,
|
|
||||||
...( form.preauth_token.value && { token: form.preauth_token.value }),
|
|
||||||
...(( ! form.preauth_token.value) && { password: form.preauth_password.value }),
|
|
||||||
nonce: form.preauth_nonce.value,
|
|
||||||
json: true
|
|
||||||
})).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
||||||
{% else %}
|
|
||||||
/* make base64url string containing our payload json object */
|
|
||||||
const data = btoa(JSON.stringify({
|
|
||||||
id: form.preauth_id.value,
|
|
||||||
token: form.preauth_token.value,
|
|
||||||
nonce: form.preauth_nonce.value,
|
|
||||||
json: true
|
|
||||||
})).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
/* send our request to the server */
|
|
||||||
fetch(window.location.href, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'X-Preauth': data },
|
|
||||||
}).then((response) => {
|
|
||||||
if (response.headers.has('Location')) {
|
|
||||||
/* follow redirect (not needed in most browsers) */
|
|
||||||
window.location.href = response.headers.get('Location');
|
|
||||||
} else if (response.headers.get('Content-Type') === 'application/json') {
|
|
||||||
/* got json, update the page */
|
|
||||||
response.json().then((content) => {
|
|
||||||
if (Object.hasOwn(content, 'message')) {
|
|
||||||
message.innerText = content.message;
|
|
||||||
}
|
|
||||||
if (Object.hasOwn(content, 'nonce')) {
|
|
||||||
form.preauth_nonce.value = content.nonce;
|
|
||||||
form.preauth_token.value = '';
|
|
||||||
form.preauth_token.focus();
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
console.log('failed to parse json from response');
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
} else { /* non-json, non-redirect response */
|
|
||||||
/* overwrite the page */
|
|
||||||
response.text().then((text) => {
|
|
||||||
document.open();
|
|
||||||
document.write(text);
|
|
||||||
document.close();
|
|
||||||
}).catch((error) => {
|
|
||||||
console.log('failed to get text from response');
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user