2fa
This commit is contained in:
parent
9906737f39
commit
d9ec021f3c
@ -14,6 +14,9 @@ MONGODB_DB=psc
|
|||||||
###> psc ###
|
###> psc ###
|
||||||
dir_market_motiv=market/motive/
|
dir_market_motiv=market/motive/
|
||||||
dir_templateprint=market/templateprint/
|
dir_templateprint=market/templateprint/
|
||||||
|
# Release-Feed (RSS/Atom) und optionaler Gitea-Token (leer = anonym/öffentlich)
|
||||||
|
PSC_RELEASES_FEED=https://git.thomas-peterson.de/boonkerz/printshopcreator/releases.rss
|
||||||
|
PSC_RELEASES_TOKEN=
|
||||||
###< psc ###
|
###< psc ###
|
||||||
|
|
||||||
###> nelmio/cors-bundle ###
|
###> nelmio/cors-bundle ###
|
||||||
|
|||||||
@ -62,6 +62,9 @@
|
|||||||
"psc/calc": "dev-master",
|
"psc/calc": "dev-master",
|
||||||
"ramsey/uuid": "4.5.1",
|
"ramsey/uuid": "4.5.1",
|
||||||
"sauladam/shipment-tracker": "dev-master",
|
"sauladam/shipment-tracker": "dev-master",
|
||||||
|
"scheb/2fa-bundle": "^8.6",
|
||||||
|
"scheb/2fa-email": "^8.6",
|
||||||
|
"scheb/2fa-totp": "^8.6",
|
||||||
"scssphp/scssphp": "v1.11.1",
|
"scssphp/scssphp": "v1.11.1",
|
||||||
"setasign/fpdi-tcpdf": "^2.3",
|
"setasign/fpdi-tcpdf": "^2.3",
|
||||||
"sofort/sofortlib-php": "3.3.2",
|
"sofort/sofortlib-php": "3.3.2",
|
||||||
|
|||||||
306
src/new/composer.lock
generated
306
src/new/composer.lock
generated
@ -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": "10c58d73f259e7dfc5d7f7ddf71a0949",
|
"content-hash": "e04b81bc87a7231b11cc580757ba49a0",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "apimatic/core",
|
"name": "apimatic/core",
|
||||||
@ -8154,6 +8154,179 @@
|
|||||||
},
|
},
|
||||||
"time": "2022-06-15T11:04:39+00:00"
|
"time": "2022-06-15T11:04:39+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "scheb/2fa-bundle",
|
||||||
|
"version": "v8.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/scheb/2fa-bundle.git",
|
||||||
|
"reference": "e5f8793a8180d874d8a96b600cd6b95b25bd098f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/e5f8793a8180d874d8a96b600cd6b95b25bd098f",
|
||||||
|
"reference": "e5f8793a8180d874d8a96b600cd6b95b25bd098f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"php": "~8.4.0 || ~8.5.0",
|
||||||
|
"symfony/config": "^7.4 || ^8.0",
|
||||||
|
"symfony/dependency-injection": "^7.4 || ^8.0",
|
||||||
|
"symfony/event-dispatcher": "^7.4 || ^8.0",
|
||||||
|
"symfony/framework-bundle": "^7.4 || ^8.0",
|
||||||
|
"symfony/http-foundation": "^7.4 || ^8.0",
|
||||||
|
"symfony/http-kernel": "^7.4 || ^8.0",
|
||||||
|
"symfony/property-access": "^7.4 || ^8.0",
|
||||||
|
"symfony/security-bundle": "^7.4 || ^8.0",
|
||||||
|
"symfony/service-contracts": "^2.5|^3",
|
||||||
|
"symfony/twig-bundle": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"scheb/two-factor-bundle": "*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"scheb/2fa-backup-code": "Emergency codes when you have no access to other methods",
|
||||||
|
"scheb/2fa-email": "Send codes by email",
|
||||||
|
"scheb/2fa-google-authenticator": "Google Authenticator support",
|
||||||
|
"scheb/2fa-totp": "Temporary one-time password (TOTP) support (Google Authenticator compatible)",
|
||||||
|
"scheb/2fa-trusted-device": "Trusted devices support"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Scheb\\TwoFactorBundle\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Christian Scheb",
|
||||||
|
"email": "me@christianscheb.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A generic interface to implement two-factor authentication in Symfony applications",
|
||||||
|
"homepage": "https://github.com/scheb/2fa",
|
||||||
|
"keywords": [
|
||||||
|
"2fa",
|
||||||
|
"Authentication",
|
||||||
|
"symfony",
|
||||||
|
"two-factor",
|
||||||
|
"two-step"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/scheb/2fa-bundle/tree/v8.6.0"
|
||||||
|
},
|
||||||
|
"time": "2026-06-12T18:24:27+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "scheb/2fa-email",
|
||||||
|
"version": "v8.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/scheb/2fa-email.git",
|
||||||
|
"reference": "7abeebe75193a57ea36961c5b751b451608c4e18"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/scheb/2fa-email/zipball/7abeebe75193a57ea36961c5b751b451608c4e18",
|
||||||
|
"reference": "7abeebe75193a57ea36961c5b751b451608c4e18",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "~8.4.0 || ~8.5.0",
|
||||||
|
"scheb/2fa-bundle": "self.version"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"symfony/mailer": "Needed if you want to use the default mailer implementation"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Scheb\\TwoFactorBundle\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Christian Scheb",
|
||||||
|
"email": "me@christianscheb.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Extends scheb/2fa-bundle with two-factor authentication via email",
|
||||||
|
"homepage": "https://github.com/scheb/2fa",
|
||||||
|
"keywords": [
|
||||||
|
"2fa",
|
||||||
|
"Authentication",
|
||||||
|
"email",
|
||||||
|
"symfony",
|
||||||
|
"two-factor",
|
||||||
|
"two-step"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/scheb/2fa-email/tree/v8.6.0"
|
||||||
|
},
|
||||||
|
"time": "2026-06-02T20:21:59+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "scheb/2fa-totp",
|
||||||
|
"version": "v8.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/scheb/2fa-totp.git",
|
||||||
|
"reference": "ca7562c6b6f9e5bb30cadcc98123327c2540e18f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/scheb/2fa-totp/zipball/ca7562c6b6f9e5bb30cadcc98123327c2540e18f",
|
||||||
|
"reference": "ca7562c6b6f9e5bb30cadcc98123327c2540e18f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "~8.4.0 || ~8.5.0",
|
||||||
|
"scheb/2fa-bundle": "self.version",
|
||||||
|
"spomky-labs/otphp": "^11.4"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"symfony/validator": "Needed if you want to use the TOTP validator constraint"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Scheb\\TwoFactorBundle\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Christian Scheb",
|
||||||
|
"email": "me@christianscheb.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Extends scheb/2fa-bundle with two-factor authentication using TOTP",
|
||||||
|
"homepage": "https://github.com/scheb/2fa",
|
||||||
|
"keywords": [
|
||||||
|
"2fa",
|
||||||
|
"Authentication",
|
||||||
|
"symfony",
|
||||||
|
"totp",
|
||||||
|
"two-factor",
|
||||||
|
"two-step"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/scheb/2fa-totp/tree/v8.6.0"
|
||||||
|
},
|
||||||
|
"time": "2026-01-24T13:27:55+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "scssphp/scssphp",
|
"name": "scssphp/scssphp",
|
||||||
"version": "v1.11.1",
|
"version": "v1.11.1",
|
||||||
@ -8674,6 +8847,76 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-06-10T07:13:16+00:00"
|
"time": "2025-06-10T07:13:16+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spomky-labs/otphp",
|
||||||
|
"version": "11.5.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Spomky-Labs/otphp.git",
|
||||||
|
"reference": "877683d6352b80cdc7020fd43a725629c2524435"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/877683d6352b80cdc7020fd43a725629c2524435",
|
||||||
|
"reference": "877683d6352b80cdc7020fd43a725629c2524435",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
|
||||||
|
"php": ">=8.1",
|
||||||
|
"psr/clock": "^1.0",
|
||||||
|
"symfony/deprecation-contracts": "^3.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/error-handler": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"OTPHP\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Florent Morselli",
|
||||||
|
"homepage": "https://github.com/Spomky"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All contributors",
|
||||||
|
"homepage": "https://github.com/Spomky-Labs/otphp/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
|
||||||
|
"homepage": "https://github.com/Spomky-Labs/otphp",
|
||||||
|
"keywords": [
|
||||||
|
"FreeOTP",
|
||||||
|
"RFC 4226",
|
||||||
|
"RFC 6238",
|
||||||
|
"google authenticator",
|
||||||
|
"hotp",
|
||||||
|
"otp",
|
||||||
|
"totp"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Spomky-Labs/otphp/issues",
|
||||||
|
"source": "https://github.com/Spomky-Labs/otphp/tree/11.5.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Spomky",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/FlorentMorselli",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-06-06T23:41:24+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/ai-agent",
|
"name": "symfony/ai-agent",
|
||||||
"version": "v0.5.0",
|
"version": "v0.5.0",
|
||||||
@ -19411,67 +19654,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-11-17T20:03:58+00:00"
|
"time": "2025-11-17T20:03:58+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "tomasvotruba/symfony-config-generator",
|
|
||||||
"version": "0.1.10",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/TomasVotruba/symfony-config-generator.git",
|
|
||||||
"reference": "2230f0a4838afc49ef50f0b12d02b3027d0c5332"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/TomasVotruba/symfony-config-generator/zipball/2230f0a4838afc49ef50f0b12d02b3027d0c5332",
|
|
||||||
"reference": "2230f0a4838afc49ef50f0b12d02b3027d0c5332",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^8.0",
|
|
||||||
"symfony/config": "^5.4|^6.4|^7.0",
|
|
||||||
"symfony/console": "^5.4|^6.4|^7.0",
|
|
||||||
"symfony/dependency-injection": "^5.4|^6.4|^7.0",
|
|
||||||
"webmozart/assert": "^1.11"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpstan/phpstan": "^1.10.56",
|
|
||||||
"rector/rector": "^1.0",
|
|
||||||
"symplify/easy-coding-standard": "^12.1",
|
|
||||||
"symplify/phpstan-rules": "^12.4",
|
|
||||||
"tomasvotruba/class-leak": "^0.2",
|
|
||||||
"tracy/tracy": "^2.10"
|
|
||||||
},
|
|
||||||
"bin": [
|
|
||||||
"bin/symfony-config-generator",
|
|
||||||
"bin/symfony-config-generator.php"
|
|
||||||
],
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"TomasVotruba\\SymfonyConfigGenerator\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "Generate Symfony 5.3+ config builder classes using CLI to improve static analysis and IDE support",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/TomasVotruba/symfony-config-generator/issues",
|
|
||||||
"source": "https://github.com/TomasVotruba/symfony-config-generator/tree/0.1.10"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://www.paypal.me/rectorphp",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/tomasvotruba",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"abandoned": "rector/swiss-knife",
|
|
||||||
"time": "2024-04-10T10:52:45+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "vincentlanglet/twig-cs-fixer",
|
"name": "vincentlanglet/twig-cs-fixer",
|
||||||
"version": "3.14.0",
|
"version": "3.14.0",
|
||||||
|
|||||||
@ -65,4 +65,5 @@ return [
|
|||||||
Symfony\UX\Vue\VueBundle::class => ['all' => true],
|
Symfony\UX\Vue\VueBundle::class => ['all' => true],
|
||||||
Spiriit\Bundle\FormFilterBundle\SpiriitFormFilterBundle::class => ['all' => true],
|
Spiriit\Bundle\FormFilterBundle\SpiriitFormFilterBundle::class => ['all' => true],
|
||||||
Symfony\AI\AiBundle\AiBundle::class => ['all' => true],
|
Symfony\AI\AiBundle\AiBundle::class => ['all' => true],
|
||||||
|
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
22
src/new/config/packages/scheb_2fa.yaml
Normal file
22
src/new/config/packages/scheb_2fa.yaml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html
|
||||||
|
scheb_two_factor:
|
||||||
|
security_tokens:
|
||||||
|
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
|
||||||
|
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
||||||
|
|
||||||
|
# E-Mail-Code-Verfahren (Code geht an das separate 2FA-Feld des Benutzers)
|
||||||
|
email:
|
||||||
|
enabled: true
|
||||||
|
sender_email: noreply@printshopcreator.de
|
||||||
|
sender_name: PrintshopCreator
|
||||||
|
digits: 6
|
||||||
|
|
||||||
|
# TOTP / Authenticator-App
|
||||||
|
totp:
|
||||||
|
enabled: true
|
||||||
|
server_name: PrintshopCreator
|
||||||
|
issuer: PrintshopCreator
|
||||||
|
parameters:
|
||||||
|
algorithm: sha1
|
||||||
|
digits: 6
|
||||||
|
period: 30
|
||||||
@ -101,6 +101,11 @@ return static function (ContainerConfigurator $containerConfigurator): void {
|
|||||||
'username_parameter' => 'username',
|
'username_parameter' => 'username',
|
||||||
'password_parameter' => 'password',
|
'password_parameter' => 'password',
|
||||||
],
|
],
|
||||||
|
'two_factor' => [
|
||||||
|
'auth_form_path' => '2fa_login',
|
||||||
|
'check_path' => '2fa_login_check',
|
||||||
|
'enable_csrf' => true,
|
||||||
|
],
|
||||||
'logout' => [
|
'logout' => [
|
||||||
'invalidate_session' => false,
|
'invalidate_session' => false,
|
||||||
'path' => 'psc_backend_logout',
|
'path' => 'psc_backend_logout',
|
||||||
@ -163,6 +168,12 @@ return static function (ContainerConfigurator $containerConfigurator): void {
|
|||||||
'path' => '^/backend/order/detail/package/download',
|
'path' => '^/backend/order/detail/package/download',
|
||||||
'roles' => 'PUBLIC_ACCESS',
|
'roles' => 'PUBLIC_ACCESS',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
// Nur die 2FA-Challenge-Routen (/backend/2fa und /backend/2fa_check),
|
||||||
|
// NICHT /backend/2fa-settings (das braucht volle Authentifizierung).
|
||||||
|
'path' => '^/backend/2fa(_check)?$',
|
||||||
|
'roles' => 'IS_AUTHENTICATED_2FA_IN_PROGRESS',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'path' => '^/backend',
|
'path' => '^/backend',
|
||||||
'roles' => 'ROLE_SHOP',
|
'roles' => 'ROLE_SHOP',
|
||||||
|
|||||||
@ -1910,6 +1910,28 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* success_handler?: scalar|Param|null,
|
* success_handler?: scalar|Param|null,
|
||||||
* failure_handler?: scalar|Param|null,
|
* failure_handler?: scalar|Param|null,
|
||||||
* },
|
* },
|
||||||
|
* two_factor?: array{
|
||||||
|
* check_path?: scalar|Param|null, // Default: "/2fa_check"
|
||||||
|
* post_only?: bool|Param, // Default: true
|
||||||
|
* auth_form_path?: scalar|Param|null, // Default: "/2fa"
|
||||||
|
* always_use_default_target_path?: bool|Param, // Default: false
|
||||||
|
* default_target_path?: scalar|Param|null, // Default: "/"
|
||||||
|
* success_handler?: scalar|Param|null, // Default: null
|
||||||
|
* failure_handler?: scalar|Param|null, // Default: null
|
||||||
|
* authentication_required_handler?: scalar|Param|null, // Default: null
|
||||||
|
* auth_code_parameter_name?: scalar|Param|null, // Default: "_auth_code"
|
||||||
|
* trusted_parameter_name?: scalar|Param|null, // Default: "_trusted"
|
||||||
|
* remember_me_sets_trusted?: scalar|Param|null, // Default: false
|
||||||
|
* multi_factor?: bool|Param, // Default: false
|
||||||
|
* prepare_on_login?: bool|Param, // Default: false
|
||||||
|
* prepare_on_access_denied?: bool|Param, // Default: false
|
||||||
|
* enable_csrf?: scalar|Param|null, // Default: false
|
||||||
|
* csrf_parameter?: scalar|Param|null, // Default: "_csrf_token"
|
||||||
|
* csrf_token_id?: scalar|Param|null, // Default: "two_factor"
|
||||||
|
* csrf_header?: scalar|Param|null, // Default: null
|
||||||
|
* csrf_token_manager?: scalar|Param|null, // Default: "scheb_two_factor.csrf_token_manager"
|
||||||
|
* provider?: scalar|Param|null, // Default: null
|
||||||
|
* },
|
||||||
* }>,
|
* }>,
|
||||||
* access_control?: list<array{ // Default: []
|
* access_control?: list<array{ // Default: []
|
||||||
* request_matcher?: scalar|Param|null, // Default: null
|
* request_matcher?: scalar|Param|null, // Default: null
|
||||||
@ -2932,6 +2954,38 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* store?: string|Param, // Service name of store // Default: "Symfony\\AI\\Store\\StoreInterface"
|
* store?: string|Param, // Service name of store // Default: "Symfony\\AI\\Store\\StoreInterface"
|
||||||
* }>,
|
* }>,
|
||||||
* }
|
* }
|
||||||
|
* @psalm-type SchebTwoFactorConfig = array{
|
||||||
|
* persister?: scalar|Param|null, // Default: "scheb_two_factor.persister.doctrine"
|
||||||
|
* model_manager_name?: scalar|Param|null, // Default: null
|
||||||
|
* security_tokens?: list<scalar|Param|null>,
|
||||||
|
* ip_whitelist?: list<scalar|Param|null>,
|
||||||
|
* ip_whitelist_provider?: scalar|Param|null, // Default: "scheb_two_factor.default_ip_whitelist_provider"
|
||||||
|
* two_factor_token_factory?: scalar|Param|null, // Default: "scheb_two_factor.default_token_factory"
|
||||||
|
* two_factor_provider_decider?: scalar|Param|null, // Default: "scheb_two_factor.default_provider_decider"
|
||||||
|
* two_factor_condition?: scalar|Param|null, // Default: null
|
||||||
|
* code_reuse_cache?: scalar|Param|null, // Default: null
|
||||||
|
* code_reuse_cache_duration?: int|Param, // Default: 60
|
||||||
|
* code_reuse_default_handler?: scalar|Param|null, // Default: null
|
||||||
|
* email?: bool|array{
|
||||||
|
* enabled?: scalar|Param|null, // Default: false
|
||||||
|
* mailer?: scalar|Param|null, // Default: null
|
||||||
|
* code_generator?: scalar|Param|null, // Default: "scheb_two_factor.security.email.default_code_generator"
|
||||||
|
* form_renderer?: scalar|Param|null, // Default: null
|
||||||
|
* sender_email?: scalar|Param|null, // Default: null
|
||||||
|
* sender_name?: scalar|Param|null, // Default: null
|
||||||
|
* template?: scalar|Param|null, // Default: "@SchebTwoFactor/Authentication/form.html.twig"
|
||||||
|
* digits?: int|Param, // Default: 4
|
||||||
|
* },
|
||||||
|
* totp?: bool|array{
|
||||||
|
* enabled?: scalar|Param|null, // Default: false
|
||||||
|
* form_renderer?: scalar|Param|null, // Default: null
|
||||||
|
* issuer?: scalar|Param|null, // Default: null
|
||||||
|
* server_name?: scalar|Param|null, // Default: null
|
||||||
|
* leeway?: int|Param, // Default: 0
|
||||||
|
* parameters?: list<scalar|Param|null>,
|
||||||
|
* template?: scalar|Param|null, // Default: "@SchebTwoFactor/Authentication/form.html.twig"
|
||||||
|
* },
|
||||||
|
* }
|
||||||
* @psalm-type ConfigType = array{
|
* @psalm-type ConfigType = array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
* parameters?: ParametersConfig,
|
* parameters?: ParametersConfig,
|
||||||
@ -2963,6 +3017,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* vue?: VueConfig,
|
* vue?: VueConfig,
|
||||||
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
||||||
* ai?: AiConfig,
|
* ai?: AiConfig,
|
||||||
|
* scheb_two_factor?: SchebTwoFactorConfig,
|
||||||
* "when@dev"?: array{
|
* "when@dev"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
* parameters?: ParametersConfig,
|
* parameters?: ParametersConfig,
|
||||||
@ -2999,6 +3054,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* vue?: VueConfig,
|
* vue?: VueConfig,
|
||||||
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
||||||
* ai?: AiConfig,
|
* ai?: AiConfig,
|
||||||
|
* scheb_two_factor?: SchebTwoFactorConfig,
|
||||||
* },
|
* },
|
||||||
* "when@prod"?: array{
|
* "when@prod"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
@ -3031,6 +3087,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* vue?: VueConfig,
|
* vue?: VueConfig,
|
||||||
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
||||||
* ai?: AiConfig,
|
* ai?: AiConfig,
|
||||||
|
* scheb_two_factor?: SchebTwoFactorConfig,
|
||||||
* },
|
* },
|
||||||
* "when@test"?: array{
|
* "when@test"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
@ -3068,6 +3125,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* vue?: VueConfig,
|
* vue?: VueConfig,
|
||||||
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
* spiriit_form_filter?: SpiriitFormFilterConfig,
|
||||||
* ai?: AiConfig,
|
* ai?: AiConfig,
|
||||||
|
* scheb_two_factor?: SchebTwoFactorConfig,
|
||||||
* },
|
* },
|
||||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
|
|||||||
7
src/new/config/routes/scheb_2fa.yaml
Normal file
7
src/new/config/routes/scheb_2fa.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
2fa_login:
|
||||||
|
path: /backend/2fa
|
||||||
|
defaults:
|
||||||
|
_controller: "scheb_two_factor.form_controller::form"
|
||||||
|
|
||||||
|
2fa_login_check:
|
||||||
|
path: /backend/2fa_check
|
||||||
@ -178,6 +178,7 @@ class DashboardController extends AbstractController
|
|||||||
'currentVersion' => $version->getRelease(),
|
'currentVersion' => $version->getRelease(),
|
||||||
'latestVersion' => $this->isGranted('ROLE_ADMIN') ? $version->getLatestRelease() : null,
|
'latestVersion' => $this->isGranted('ROLE_ADMIN') ? $version->getLatestRelease() : null,
|
||||||
'updateAvailable' => $this->isGranted('ROLE_ADMIN') ? $version->isUpdateAvailable() : false,
|
'updateAvailable' => $this->isGranted('ROLE_ADMIN') ? $version->isUpdateAvailable() : false,
|
||||||
|
'newerReleases' => $this->isGranted('ROLE_ADMIN') ? $version->getNewerReleases() : [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PSC\Backend\DashboardBundle\Controller;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PSC\Shop\EntityBundle\Entity\Contact;
|
||||||
|
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self-Service-Verwaltung der Zwei-Faktor-Authentifizierung (optional pro Benutzer).
|
||||||
|
*/
|
||||||
|
#[IsGranted('ROLE_SHOP')]
|
||||||
|
class TwoFactorController extends AbstractController
|
||||||
|
{
|
||||||
|
private const SESSION_PENDING_SECRET = '2fa_pending_totp_secret';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly TotpAuthenticatorInterface $totpAuthenticator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/2fa-settings', name: 'psc_backend_2fa_settings')]
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
/** @var Contact $user */
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
$pendingSecret = $request->getSession()->get(self::SESSION_PENDING_SECRET);
|
||||||
|
$qrContent = null;
|
||||||
|
if ($pendingSecret) {
|
||||||
|
// Secret nur transient setzen (kein flush) um den QR-Inhalt zu erzeugen.
|
||||||
|
$user->setTotpSecret($pendingSecret);
|
||||||
|
$qrContent = $this->totpAuthenticator->getQRContent($user);
|
||||||
|
$user->setTotpSecret(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@PSCBackendDashboard/security/2fa.html.twig', [
|
||||||
|
'emailEnabled' => $user->isTwoFactorEmailEnabled(),
|
||||||
|
'twoFactorEmail' => $user->getTwoFactorEmail(),
|
||||||
|
'loginEmail' => $user->getEmail(),
|
||||||
|
'totpEnabled' => $user->isTotpAuthenticationEnabled(),
|
||||||
|
'pendingSecret' => $pendingSecret,
|
||||||
|
'qrContent' => $qrContent,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/2fa-settings/email', name: 'psc_backend_2fa_email', methods: ['POST'])]
|
||||||
|
public function email(Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->isCsrfTokenValid('2fa_email', (string) $request->request->get('_token'))) {
|
||||||
|
$this->addFlash('error', 'Ungültiges Formular (CSRF).');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Contact $user */
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
$enabled = (bool) $request->request->get('enabled');
|
||||||
|
$user->setTwoFactorEmail($request->request->get('two_factor_email'));
|
||||||
|
$user->setTwoFactorEmailEnabled($enabled);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', $enabled
|
||||||
|
? 'E-Mail-2FA aktiviert.'
|
||||||
|
: 'E-Mail-2FA deaktiviert.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/2fa-settings/totp/start', name: 'psc_backend_2fa_totp_start', methods: ['POST'])]
|
||||||
|
public function totpStart(Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->isCsrfTokenValid('2fa_totp_start', (string) $request->request->get('_token'))) {
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->getSession()->set(self::SESSION_PENDING_SECRET, $this->totpAuthenticator->generateSecret());
|
||||||
|
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/2fa-settings/totp/confirm', name: 'psc_backend_2fa_totp_confirm', methods: ['POST'])]
|
||||||
|
public function totpConfirm(Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->isCsrfTokenValid('2fa_totp_confirm', (string) $request->request->get('_token'))) {
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Contact $user */
|
||||||
|
$user = $this->getUser();
|
||||||
|
$pendingSecret = $request->getSession()->get(self::SESSION_PENDING_SECRET);
|
||||||
|
$code = trim((string) $request->request->get('code'));
|
||||||
|
|
||||||
|
if (!$pendingSecret) {
|
||||||
|
$this->addFlash('error', 'Keine Einrichtung aktiv. Bitte erneut starten.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code gegen das ausstehende Secret prüfen (transient gesetzt).
|
||||||
|
$user->setTotpSecret($pendingSecret);
|
||||||
|
$valid = $this->totpAuthenticator->checkCode($user, $code);
|
||||||
|
|
||||||
|
if (!$valid) {
|
||||||
|
$user->setTotpSecret(null);
|
||||||
|
$this->addFlash('error', 'Code ungültig. Bitte erneut versuchen.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gültig: Secret dauerhaft speichern, Einrichtung abschließen.
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$request->getSession()->remove(self::SESSION_PENDING_SECRET);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'TOTP / Authenticator-App aktiviert.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/2fa-settings/totp/disable', name: 'psc_backend_2fa_totp_disable', methods: ['POST'])]
|
||||||
|
public function totpDisable(Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->isCsrfTokenValid('2fa_totp_disable', (string) $request->request->get('_token'))) {
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Contact $user */
|
||||||
|
$user = $this->getUser();
|
||||||
|
$user->setTotpSecret(null);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$request->getSession()->remove(self::SESSION_PENDING_SECRET);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'TOTP deaktiviert.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('psc_backend_2fa_settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,6 +56,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 lg:space-y-8">
|
||||||
|
{% if newerReleases is not empty %}
|
||||||
|
<div class="rounded-sm border border bg-white px-5 py-4 shadow-lg dark:border-strokedark dark:bg-boxdark">
|
||||||
|
<div class="px-2 pt-1 pb-2">
|
||||||
|
<h2 class="text-lg font-medium tracking-tight filament-card-heading mb-2">
|
||||||
|
Verfügbare Updates
|
||||||
|
</h2>
|
||||||
|
<div aria-hidden="true" class="filament-hr border-t dark:border-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="px-2 py-1 divide-y dark:divide-gray-700 text-sm">
|
||||||
|
{% for release in newerReleases %}
|
||||||
|
<li class="flex items-center justify-between gap-2 py-2">
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<a href="{{ release.url }}" target="_blank" rel="noopener" class="text-psc-500 font-medium">{{ release.name }}</a>
|
||||||
|
{% if release.prerelease %}
|
||||||
|
<span class="bg-yellow-500 text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded">Pre-Release</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% if release.publishedAt %}<span class="text-xs text-gray-400">{{ release.publishedAt|date('d.m.Y') }}</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if diskUsage %}
|
{% if diskUsage %}
|
||||||
<div class="rounded-sm border border bg-white px-5 py-4 shadow-lg dark:border-strokedark dark:bg-boxdark">
|
<div class="rounded-sm border border bg-white px-5 py-4 shadow-lg dark:border-strokedark dark:bg-boxdark">
|
||||||
<div class="px-2 pt-1 pb-2">
|
<div class="px-2 pt-1 pb-2">
|
||||||
@ -157,6 +182,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-span-full">
|
<div class="col-span-full">
|
||||||
<div class="rounded-sm border border bg-white px-7.5 py-6 shadow-lg dark:border-strokedark dark:bg-boxdark">
|
<div class="rounded-sm border border bg-white px-7.5 py-6 shadow-lg dark:border-strokedark dark:bg-boxdark">
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
{% extends 'backend_tailwind_base.html.twig' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1 class="text-psc text-2xl font-medium"><i class="fas fa-shield-alt mr-1"></i>Zwei-Faktor-Authentifizierung</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="max-w-3xl flex flex-col gap-6">
|
||||||
|
|
||||||
|
{% for label, messages in app.flashes %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="rounded-sm border px-4 py-3 text-sm {% if label == 'error' %}bg-red-50 border-red-200 text-red-700{% else %}bg-green-50 border-green-200 text-green-700{% endif %}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{# ---------------- E-Mail-Verfahren ---------------- #}
|
||||||
|
<div class="rounded-sm border bg-white px-5 py-4 shadow-lg dark:border-strokedark dark:bg-boxdark">
|
||||||
|
<h2 class="text-lg font-medium mb-1">E-Mail-Code</h2>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">Bei jedem Login wird ein Einmal-Code an die unten angegebene E-Mail-Adresse geschickt.</p>
|
||||||
|
<form method="post" action="{{ path('psc_backend_2fa_email') }}" class="space-y-4">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('2fa_email') }}">
|
||||||
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<input type="checkbox" name="enabled" value="1" {{ emailEnabled ? 'checked' : '' }} class="rounded border-gray-300">
|
||||||
|
E-Mail-2FA aktivieren
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">E-Mail für den Sicherheitscode</label>
|
||||||
|
<input type="email" name="two_factor_email" value="{{ twoFactorEmail }}" placeholder="{{ loginEmail }}"
|
||||||
|
class="w-full rounded-md border border-gray-300 p-2 text-sm">
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Leer = Login-Adresse ({{ loginEmail }}) verwenden.</p>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-sm text-sm font-medium text-white bg-psc-500 hover:bg-psc-600 shadow-sm">Speichern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ---------------- TOTP / Authenticator ---------------- #}
|
||||||
|
<div class="rounded-sm border bg-white px-5 py-4 shadow-lg dark:border-strokedark dark:bg-boxdark">
|
||||||
|
<h2 class="text-lg font-medium mb-1">Authenticator-App (TOTP)</h2>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">Zeitbasierte Codes über Google/Microsoft Authenticator o.ä.</p>
|
||||||
|
|
||||||
|
{% if totpEnabled %}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded">Aktiviert</span>
|
||||||
|
<form method="post" action="{{ path('psc_backend_2fa_totp_disable') }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('2fa_totp_disable') }}">
|
||||||
|
<button type="submit" class="inline-flex items-center px-3 py-1.5 rounded-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 shadow-sm">Deaktivieren</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% elseif pendingSecret %}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<p class="text-sm">1. Scanne den QR-Code mit deiner Authenticator-App (oder gib den Schlüssel manuell ein):</p>
|
||||||
|
<div id="totp-qr" class="bg-white p-3 inline-block w-fit rounded border border-gray-200"></div>
|
||||||
|
<p class="text-xs text-gray-500">Schlüssel: <code class="bg-gray-100 px-1.5 py-0.5 rounded">{{ pendingSecret }}</code></p>
|
||||||
|
<form method="post" action="{{ path('psc_backend_2fa_totp_confirm') }}" class="space-y-3">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('2fa_totp_confirm') }}">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">2. Code aus der App eingeben</label>
|
||||||
|
<input type="text" name="code" inputmode="numeric" autocomplete="one-time-code" placeholder="123456"
|
||||||
|
class="w-40 rounded-md border border-gray-300 p-2 text-sm tracking-widest">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-sm text-sm font-medium text-white bg-psc-500 hover:bg-psc-600 shadow-sm">Bestätigen & aktivieren</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
new QRCode(document.getElementById('totp-qr'), { text: {{ qrContent|json_encode|raw }}, width: 200, height: 200 });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="{{ path('psc_backend_2fa_totp_start') }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('2fa_totp_start') }}">
|
||||||
|
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-sm text-sm font-medium text-white bg-psc-500 hover:bg-psc-600 shadow-sm">Einrichtung starten</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -18,6 +18,10 @@ use Doctrine\Common\Collections\Collection;
|
|||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use PSC\Shop\ContactBundle\Model\AccountType;
|
use PSC\Shop\ContactBundle\Model\AccountType;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface as EmailTwoFactorInterface;
|
||||||
|
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
|
||||||
|
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
|
||||||
|
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface as TotpTwoFactorInterface;
|
||||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
@ -30,7 +34,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
*/
|
*/
|
||||||
#[ORM\Table(name: 'contact')]
|
#[ORM\Table(name: 'contact')]
|
||||||
#[ORM\Entity(repositoryClass: 'PSC\Shop\ContactBundle\Repository\ContactRepository')]
|
#[ORM\Entity(repositoryClass: 'PSC\Shop\ContactBundle\Repository\ContactRepository')]
|
||||||
class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Serializable
|
class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Serializable, EmailTwoFactorInterface, TotpTwoFactorInterface
|
||||||
{
|
{
|
||||||
public $kundenNr = '';
|
public $kundenNr = '';
|
||||||
public $calcValue1 = '';
|
public $calcValue1 = '';
|
||||||
@ -111,6 +115,24 @@ class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
|||||||
#[ORM\Column(name: 'uuid', type: 'string', length: 40)]
|
#[ORM\Column(name: 'uuid', type: 'string', length: 40)]
|
||||||
public $uuid;
|
public $uuid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwei-Faktor-Authentifizierung (Backend) – Felder. Optionales Opt-in pro Benutzer.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(name: 'two_factor_email_enabled', type: 'boolean', nullable: true)]
|
||||||
|
protected ?bool $twoFactorEmailEnabled = null;
|
||||||
|
|
||||||
|
/** Separate E-Mail-Adresse, an die der 2FA-Code geschickt wird. */
|
||||||
|
#[ORM\Column(name: 'two_factor_email', type: 'string', length: 255, nullable: true)]
|
||||||
|
protected ?string $twoFactorEmail = null;
|
||||||
|
|
||||||
|
/** Aktuell generierter E-Mail-Auth-Code (von scheb verwaltet). */
|
||||||
|
#[ORM\Column(name: 'email_auth_code', type: 'string', length: 255, nullable: true)]
|
||||||
|
protected ?string $emailAuthCode = null;
|
||||||
|
|
||||||
|
/** TOTP-Secret (gesetzt = TOTP aktiviert). */
|
||||||
|
#[ORM\Column(name: 'totp_secret', type: 'string', length: 255, nullable: true)]
|
||||||
|
protected ?string $totpSecret = null;
|
||||||
|
|
||||||
#[ORM\JoinTable(name: 'contact_role')]
|
#[ORM\JoinTable(name: 'contact_role')]
|
||||||
#[ORM\JoinColumn(name: 'contact_id', referencedColumnName: 'id')]
|
#[ORM\JoinColumn(name: 'contact_id', referencedColumnName: 'id')]
|
||||||
#[ORM\InverseJoinColumn(name: 'role_id', referencedColumnName: 'id')]
|
#[ORM\InverseJoinColumn(name: 'role_id', referencedColumnName: 'id')]
|
||||||
@ -333,6 +355,80 @@ class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
|||||||
//$this->setPassword("");
|
//$this->setPassword("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== 2FA: E-Mail-Verfahren (scheb) ===================== */
|
||||||
|
|
||||||
|
public function isEmailAuthEnabled(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->twoFactorEmailEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailAuthRecipient(): string
|
||||||
|
{
|
||||||
|
// Code geht an das separate 2FA-Feld; Fallback auf die Login-E-Mail.
|
||||||
|
return $this->twoFactorEmail ?: (string) $this->getEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailAuthCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->emailAuthCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmailAuthCode(string $authCode): void
|
||||||
|
{
|
||||||
|
$this->emailAuthCode = $authCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTwoFactorEmailEnabled(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->twoFactorEmailEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTwoFactorEmailEnabled(?bool $enabled): void
|
||||||
|
{
|
||||||
|
$this->twoFactorEmailEnabled = $enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTwoFactorEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->twoFactorEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTwoFactorEmail(?string $email): void
|
||||||
|
{
|
||||||
|
$this->twoFactorEmail = $email !== null && trim($email) !== '' ? trim($email) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 2FA: TOTP-Verfahren (scheb) ====================== */
|
||||||
|
|
||||||
|
public function isTotpAuthenticationEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->totpSecret !== null && $this->totpSecret !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotpAuthenticationUsername(): string
|
||||||
|
{
|
||||||
|
return (string) $this->getEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
|
||||||
|
{
|
||||||
|
if (!$this->isTotpAuthenticationEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotpSecret(): ?string
|
||||||
|
{
|
||||||
|
return $this->totpSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTotpSecret(?string $secret): void
|
||||||
|
{
|
||||||
|
$this->totpSecret = $secret;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt den Namen zurück
|
* Gibt den Namen zurück
|
||||||
*
|
*
|
||||||
|
|||||||
@ -4,6 +4,9 @@ services:
|
|||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
bind:
|
bind:
|
||||||
$projectDir: '%kernel.project_dir%'
|
$projectDir: '%kernel.project_dir%'
|
||||||
|
# Release-Feed-URL und optionaler Gitea-Token (privat). Leer = anonym.
|
||||||
|
$releasesFeed: '%env(PSC_RELEASES_FEED)%'
|
||||||
|
$releasesToken: '%env(PSC_RELEASES_TOKEN)%'
|
||||||
|
|
||||||
PSC\System\SettingsBundle\:
|
PSC\System\SettingsBundle\:
|
||||||
resource: '../../*/*'
|
resource: '../../*/*'
|
||||||
|
|||||||
@ -10,17 +10,14 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|||||||
|
|
||||||
class Version
|
class Version
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* RSS-Feed mit den veröffentlichten Releases.
|
|
||||||
*/
|
|
||||||
private const RELEASES_FEED = 'https://git.thomas-peterson.de/boonkerz/printshopcreator/releases.rss';
|
|
||||||
|
|
||||||
private ?array $data = null;
|
private ?array $data = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly KernelInterface $kernel,
|
private readonly KernelInterface $kernel,
|
||||||
private readonly HttpClientInterface $httpClient,
|
private readonly HttpClientInterface $httpClient,
|
||||||
private readonly CacheInterface $cache,
|
private readonly CacheInterface $cache,
|
||||||
|
private readonly string $releasesFeed = 'https://git.thomas-peterson.de/boonkerz/printshopcreator/releases.rss',
|
||||||
|
private readonly string $releasesToken = '',
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private function load(): void
|
private function load(): void
|
||||||
@ -49,49 +46,90 @@ class Version
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Neueste verfügbare Version aus dem Release-RSS-Feed.
|
* Alle veröffentlichten Releases aus der Gitea-API (inkl. Pre-Release-Flag).
|
||||||
* Ergebnis wird zwischengespeichert, damit das Dashboard schnell bleibt und
|
* Wird zwischengespeichert, damit das Dashboard schnell bleibt. Bei Fehlern: [].
|
||||||
* der Feed nicht bei jedem Aufruf abgefragt wird. Bei Fehlern: null.
|
*
|
||||||
|
* @return list<array{version:string,name:string,tag:string,prerelease:bool,url:string,publishedAt:?string}>
|
||||||
|
*/
|
||||||
|
public function getReleases(): array
|
||||||
|
{
|
||||||
|
return $this->cache->get('psc_releases_list', function (ItemInterface $item): array {
|
||||||
|
$item->expiresAfter(3600);
|
||||||
|
|
||||||
|
$api = $this->getApiUrl();
|
||||||
|
if ($api === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$options = ['timeout' => 5, 'query' => ['limit' => 30]];
|
||||||
|
if ($this->releasesToken !== '') {
|
||||||
|
$options['headers'] = ['Authorization' => 'token ' . $this->releasesToken];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->httpClient->request('GET', $api, $options);
|
||||||
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$releases = [];
|
||||||
|
foreach ($response->toArray(false) as $entry) {
|
||||||
|
if (!empty($entry['draft'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tag = (string) ($entry['tag_name'] ?? $entry['name'] ?? '');
|
||||||
|
if (!preg_match('/\d+\.\d+(?:\.\d+)*/', $tag, $matches)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$releases[] = [
|
||||||
|
'version' => $matches[0],
|
||||||
|
'name' => (string) ($entry['name'] ?? $tag),
|
||||||
|
'tag' => $tag,
|
||||||
|
'prerelease' => (bool) ($entry['prerelease'] ?? false),
|
||||||
|
'url' => (string) ($entry['html_url'] ?? ''),
|
||||||
|
'publishedAt' => $entry['published_at'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $releases;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases, die neuer als die installierte Version sind (absteigend sortiert).
|
||||||
|
*
|
||||||
|
* @return list<array{version:string,name:string,tag:string,prerelease:bool,url:string,publishedAt:?string}>
|
||||||
|
*/
|
||||||
|
public function getNewerReleases(): array
|
||||||
|
{
|
||||||
|
$current = $this->getRelease();
|
||||||
|
|
||||||
|
$newer = array_values(array_filter(
|
||||||
|
$this->getReleases(),
|
||||||
|
fn (array $release): bool => version_compare($release['version'], $current, '>'),
|
||||||
|
));
|
||||||
|
|
||||||
|
usort($newer, fn (array $a, array $b): int => version_compare($b['version'], $a['version']));
|
||||||
|
|
||||||
|
return $newer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neueste verfügbare Version (höchste Versionsnummer). Bei Fehlern: null.
|
||||||
*/
|
*/
|
||||||
public function getLatestRelease(): ?string
|
public function getLatestRelease(): ?string
|
||||||
{
|
{
|
||||||
return $this->cache->get('psc_latest_release_version', function (ItemInterface $item): ?string {
|
$latest = null;
|
||||||
$item->expiresAfter(3600);
|
foreach ($this->getReleases() as $release) {
|
||||||
|
if ($latest === null || version_compare($release['version'], $latest, '>')) {
|
||||||
try {
|
$latest = $release['version'];
|
||||||
$response = $this->httpClient->request('GET', self::RELEASES_FEED, [
|
|
||||||
'timeout' => 5,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$content = $response->getContent();
|
|
||||||
$xml = @simplexml_load_string($content);
|
|
||||||
|
|
||||||
if ($xml === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSS 2.0 (channel->item) bzw. Atom (entry) unterstützen.
|
|
||||||
$title = null;
|
|
||||||
if (isset($xml->channel->item[0]->title)) {
|
|
||||||
$title = (string) $xml->channel->item[0]->title;
|
|
||||||
} elseif (isset($xml->entry[0]->title)) {
|
|
||||||
$title = (string) $xml->entry[0]->title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($title === null || $title === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Versionsnummer (z.B. 2.3.7) aus dem Titel extrahieren.
|
|
||||||
if (preg_match('/\d+\.\d+(?:\.\d+)*/', $title, $matches)) {
|
|
||||||
return $matches[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ltrim(trim($title), 'vV');
|
|
||||||
} catch (\Throwable) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return $latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,12 +137,23 @@ class Version
|
|||||||
*/
|
*/
|
||||||
public function isUpdateAvailable(): bool
|
public function isUpdateAvailable(): bool
|
||||||
{
|
{
|
||||||
$latest = $this->getLatestRelease();
|
return $this->getNewerReleases() !== [];
|
||||||
|
}
|
||||||
|
|
||||||
if ($latest === null || $latest === '') {
|
/**
|
||||||
return false;
|
* Leitet die Gitea-API-URL für Releases aus der konfigurierten Feed-URL ab.
|
||||||
|
* z.B. https://host/owner/repo/releases.rss -> https://host/api/v1/repos/owner/repo/releases
|
||||||
|
*/
|
||||||
|
private function getApiUrl(): ?string
|
||||||
|
{
|
||||||
|
if ($this->releasesFeed === '') {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return version_compare($latest, $this->getRelease(), '>');
|
if (preg_match('#^(https?://[^/]+)/([^/]+)/([^/]+)/releases\.(?:rss|atom)#', $this->releasesFeed, $m)) {
|
||||||
|
return $m[1] . '/api/v1/repos/' . $m[2] . '/' . $m[3] . '/releases';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PSC\System\UpdateBundle\Migrations;
|
||||||
|
|
||||||
|
class Version20260624160000 extends Base
|
||||||
|
{
|
||||||
|
public function migrateDatabase(): void
|
||||||
|
{
|
||||||
|
$connection = $this->entityManager->getConnection();
|
||||||
|
|
||||||
|
$columns = [
|
||||||
|
'two_factor_email_enabled' => 'TINYINT(1) NULL DEFAULT NULL',
|
||||||
|
'two_factor_email' => 'VARCHAR(255) NULL DEFAULT NULL',
|
||||||
|
'email_auth_code' => 'VARCHAR(255) NULL DEFAULT NULL',
|
||||||
|
'totp_secret' => 'VARCHAR(255) NULL DEFAULT NULL',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($columns as $name => $definition) {
|
||||||
|
$exists = $connection->fetchOne(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.COLUMNS "
|
||||||
|
. "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'contact' AND COLUMN_NAME = ?",
|
||||||
|
[$name],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$exists) {
|
||||||
|
$connection->executeStatement("ALTER TABLE contact ADD COLUMN `$name` $definition");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -432,6 +432,19 @@
|
|||||||
"sauladam/shipment-tracker": {
|
"sauladam/shipment-tracker": {
|
||||||
"version": "dev-master"
|
"version": "dev-master"
|
||||||
},
|
},
|
||||||
|
"scheb/2fa-bundle": {
|
||||||
|
"version": "8.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.0",
|
||||||
|
"ref": "1e6f68089146853a790b5da9946fc5974f6fcd49"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/scheb_2fa.yaml",
|
||||||
|
"config/routes/scheb_2fa.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"scssphp/scssphp": {
|
"scssphp/scssphp": {
|
||||||
"version": "dev-master"
|
"version": "dev-master"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -82,6 +82,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if app.user and not (app.user.twoFactorEmailEnabled or app.user.totpAuthenticationEnabled) %}
|
||||||
|
<div class="flex items-center mr-2">
|
||||||
|
<a href="{{ path('psc_backend_2fa_settings') }}" class="bg-red-100 text-red-800 text-xs font-medium px-2.5 py-1 rounded whitespace-nowrap" title="Zwei-Faktor-Authentifizierung einrichten">Kein 2FA aktiviert</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
{% if app.request.locale == 'de_DE' %}
|
{% if app.request.locale == 'de_DE' %}
|
||||||
<a href="{{ app.request.uri }}?_locale=en" class="flex w-full items-center whitespace-nowrap rounded-md p-2 text-sm outline-none hover:text-white focus:text-white hover:bg-psc-500 focus:bg-psc-500">EN</a>
|
<a href="{{ app.request.uri }}?_locale=en" class="flex w-full items-center whitespace-nowrap rounded-md p-2 text-sm outline-none hover:text-white focus:text-white hover:bg-psc-500 focus:bg-psc-500">EN</a>
|
||||||
@ -120,6 +125,13 @@
|
|||||||
</button>-->
|
</button>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ path('psc_backend_2fa_settings') }}" class="block flex w-full items-center whitespace-nowrap rounded-md p-2 text-sm outline-none hover:text-white focus:text-white hover:bg-psc-500 focus:bg-psc-500" title="Zwei-Faktor-Authentifizierung">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="filament-dropdown-list-item-icon mr-2 h-5 w-5 rtl:ml-2 rtl:mr-0 group-hover:text-white group-focus:text-white text-primary-500">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||||
|
</svg>
|
||||||
|
<span class="truncate w-full text-start">2FA</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="{{ path('psc_backend_logout') }}" class="block flex w-full items-center whitespace-nowrap rounded-md p-2 text-sm outline-none hover:text-white focus:text-white hover:bg-psc-500 focus:bg-psc-500" title="Logout">
|
<a href="{{ path('psc_backend_logout') }}" class="block flex w-full items-center whitespace-nowrap rounded-md p-2 text-sm outline-none hover:text-white focus:text-white hover:bg-psc-500 focus:bg-psc-500" title="Logout">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="filament-dropdown-list-item-icon mr-2 h-5 w-5 rtl:ml-2 rtl:mr-0 group-hover:text-white group-focus:text-white text-primary-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="filament-dropdown-list-item-icon mr-2 h-5 w-5 rtl:ml-2 rtl:mr-0 group-hover:text-white group-focus:text-white text-primary-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
{% extends 'backend_login.html.twig' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="p-8 space-y-4 bg-white/50 backdrop-blur-xl border border-gray-200 shadow-2xl rounded-2xl relative">
|
||||||
|
<pre><img src="{{ asset('images/logo.png') }}" alt="Logo"></pre>
|
||||||
|
|
||||||
|
<h1 class="text-lg font-medium text-gray-800">Zwei-Faktor-Authentifizierung</h1>
|
||||||
|
|
||||||
|
{% if authenticationError %}
|
||||||
|
<div class="rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm px-3 py-2">
|
||||||
|
{{ authenticationError|trans(authenticationErrorData, 'SchebTwoFactorBundle') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if availableTwoFactorProviders|length > 1 %}
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
{{ "choose_provider"|trans({}, 'SchebTwoFactorBundle') }}:
|
||||||
|
{% for provider in availableTwoFactorProviders %}
|
||||||
|
<a class="text-psc-500 hover:underline mr-2" href="{{ path('2fa_login', {'preferProvider': provider}) }}">{{ provider }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ checkPathUrl ? checkPathUrl : path(checkPathRoute) }}">
|
||||||
|
<label class="text-sm font-medium leading-4 text-gray-700" for="_auth_code">
|
||||||
|
{{ "auth_code"|trans({}, 'SchebTwoFactorBundle') }} ({{ twoFactorProvider }})
|
||||||
|
</label>
|
||||||
|
<input id="_auth_code" type="text" name="{{ authCodeParameterName }}"
|
||||||
|
inputmode="numeric" autocomplete="one-time-code" autofocus
|
||||||
|
class="mt-1 block w-full rounded-lg shadow-sm outline-none border-gray-300 focus:border-psc-500 focus:ring-1 focus:ring-inset focus:ring-psc-500 tracking-widest" />
|
||||||
|
|
||||||
|
{% if displayTrustedOption %}
|
||||||
|
<label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
|
||||||
|
<input type="checkbox" name="{{ trustedParameterName }}" class="rounded border-gray-300">
|
||||||
|
{{ "trusted"|trans({}, 'SchebTwoFactorBundle') }}
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
{% if isCsrfProtectionEnabled %}
|
||||||
|
<input type="hidden" name="{{ csrfParameterName }}" value="{{ csrf_token(csrfTokenId) }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button type="submit" class="mt-4 inline-flex items-center justify-center font-medium rounded-lg transition-colors min-h-[2.25rem] px-4 text-sm text-white shadow w-full bg-psc-500">
|
||||||
|
{{ "login"|trans({}, 'SchebTwoFactorBundle') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-center text-sm">
|
||||||
|
<a class="text-gray-500 hover:underline" href="{{ logoutPath }}">{{ "cancel"|trans({}, 'SchebTwoFactorBundle') }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -1,8 +1,16 @@
|
|||||||
info:
|
info:
|
||||||
datum: 23.06.2026
|
datum: 24.06.2026
|
||||||
release: 2.3.7
|
release: 2.3.8
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
|
- version: 2.3.8
|
||||||
|
datum: 24.06.2026
|
||||||
|
changes:
|
||||||
|
- "Zwei-Faktor-Authentifizierung (2FA) fürs Backend – optional pro Benutzer"
|
||||||
|
- "2FA-Verfahren: TOTP (Authenticator-App) und E-Mail-Code, einzeln oder zusammen aktivierbar"
|
||||||
|
- "E-Mail-Code geht an eine separate, frei wählbare 2FA-Adresse (Fallback: Login-E-Mail)"
|
||||||
|
- "Selbstverwaltung unter /backend/2fa-settings (QR-Einrichtung für TOTP, Aktivieren/Deaktivieren)"
|
||||||
|
- "Dashboard: Anzeige der aktuellen Version sowie verfügbarer (neuerer) Releases inkl. Pre-Release-Kennzeichnung"
|
||||||
- version: 2.3.7
|
- version: 2.3.7
|
||||||
datum: 23.06.2026
|
datum: 23.06.2026
|
||||||
changes:
|
changes:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user