Implement SMTP Mail Delivery and Self-Service Password Reset (#3848)
This commit is contained in:
parent
4cd090decd
commit
c24f5dfc69
37 changed files with 1376 additions and 218 deletions
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace PHPSTORM_META {
|
namespace PHPSTORM_META {
|
||||||
|
|
||||||
override(
|
override(
|
||||||
\Psr\Container\ContainerInterface::get(0),
|
\Psr\Container\ContainerInterface::get(0),
|
||||||
map(
|
map(
|
||||||
|
@ -18,4 +17,21 @@ namespace PHPSTORM_META {
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
override(
|
||||||
|
\DI\FactoryInterface::make(0),
|
||||||
|
map(
|
||||||
|
[
|
||||||
|
'' => '@',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
override(
|
||||||
|
\DI\Container::make(0),
|
||||||
|
map(
|
||||||
|
[
|
||||||
|
'' => '@',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,12 @@ $app = App\AppFactory::create(
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @var \Psr\Container\ContainerInterface|\DI\FactoryInterface $di */
|
||||||
$di = $app->getContainer();
|
$di = $app->getContainer();
|
||||||
|
|
||||||
App\Customization::initCli();
|
/** @var \App\Locale $locale */
|
||||||
|
$locale = $di->make(\App\Locale::class);
|
||||||
|
$locale->register();
|
||||||
|
|
||||||
/** @var App\Console\Application $cli */
|
/** @var App\Console\Application $cli */
|
||||||
$cli = $di->get(App\Console\Application::class);
|
$cli = $di->get(App\Console\Application::class);
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"symfony/event-dispatcher": "^5",
|
"symfony/event-dispatcher": "^5",
|
||||||
"symfony/finder": "^5",
|
"symfony/finder": "^5",
|
||||||
"symfony/lock": "^5.1",
|
"symfony/lock": "^5.1",
|
||||||
|
"symfony/mailer": "^5.2",
|
||||||
"symfony/messenger": "^5",
|
"symfony/messenger": "^5",
|
||||||
"symfony/process": "^5",
|
"symfony/process": "^5",
|
||||||
"symfony/property-access": "^5",
|
"symfony/property-access": "^5",
|
||||||
|
|
333
composer.lock
generated
333
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": "8aed63d3424000ce9590c98912eb5a81",
|
"content-hash": "8d2dbb48bbce314e087712c31d62a372",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-sdk-php",
|
"name": "aws/aws-sdk-php",
|
||||||
|
@ -102,21 +102,20 @@
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/AzuraCast/azuraforms.git",
|
"url": "https://github.com/AzuraCast/azuraforms.git",
|
||||||
"reference": "70e51d4c1892392ad33529949f62cfb93ca5e48a"
|
"reference": "8002d78f62a34cdb14df8967136547cdf6c04083"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/AzuraCast/azuraforms/zipball/70e51d4c1892392ad33529949f62cfb93ca5e48a",
|
"url": "https://api.github.com/repos/AzuraCast/azuraforms/zipball/8002d78f62a34cdb14df8967136547cdf6c04083",
|
||||||
"reference": "70e51d4c1892392ad33529949f62cfb93ca5e48a",
|
"reference": "8002d78f62a34cdb14df8967136547cdf6c04083",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.2"
|
"php": ">=7.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"overtrue/phplint": "^1.1",
|
"overtrue/phplint": "^2.0",
|
||||||
"phpstan/phpstan": "^0.11.1",
|
"phpstan/phpstan": "^0.12",
|
||||||
"phpstan/phpstan-strict-rules": "^0.11.0",
|
|
||||||
"roave/security-advisories": "dev-master"
|
"roave/security-advisories": "dev-master"
|
||||||
},
|
},
|
||||||
"default-branch": true,
|
"default-branch": true,
|
||||||
|
@ -150,7 +149,7 @@
|
||||||
"issues": "https://github.com/AzuraCast/azuraforms/issues",
|
"issues": "https://github.com/AzuraCast/azuraforms/issues",
|
||||||
"source": "https://github.com/AzuraCast/azuraforms/tree/main"
|
"source": "https://github.com/AzuraCast/azuraforms/tree/main"
|
||||||
},
|
},
|
||||||
"time": "2021-02-11T18:01:43+00:00"
|
"time": "2021-02-24T03:51:40+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "azuracast/nowplaying",
|
"name": "azuracast/nowplaying",
|
||||||
|
@ -1836,6 +1835,74 @@
|
||||||
},
|
},
|
||||||
"time": "2020-10-24T22:13:54+00:00"
|
"time": "2020-10-24T22:13:54+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "egulias/email-validator",
|
||||||
|
"version": "2.1.25",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/egulias/EmailValidator.git",
|
||||||
|
"reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0dbf5d78455d4d6a41d186da50adc1122ec066f4",
|
||||||
|
"reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"doctrine/lexer": "^1.0.1",
|
||||||
|
"php": ">=5.5",
|
||||||
|
"symfony/polyfill-intl-idn": "^1.10"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dominicsayers/isemail": "^3.0.7",
|
||||||
|
"phpunit/phpunit": "^4.8.36|^7.5.15",
|
||||||
|
"satooshi/php-coveralls": "^1.0.1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Egulias\\EmailValidator\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Eduardo Gulias Davis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library for validating emails against several RFCs",
|
||||||
|
"homepage": "https://github.com/egulias/EmailValidator",
|
||||||
|
"keywords": [
|
||||||
|
"email",
|
||||||
|
"emailvalidation",
|
||||||
|
"emailvalidator",
|
||||||
|
"validation",
|
||||||
|
"validator"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/egulias/EmailValidator/issues",
|
||||||
|
"source": "https://github.com/egulias/EmailValidator/tree/2.1.25"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/egulias",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2020-12-29T14:50:06+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "friendsofphp/proxy-manager-lts",
|
"name": "friendsofphp/proxy-manager-lts",
|
||||||
"version": "v1.0.3",
|
"version": "v1.0.3",
|
||||||
|
@ -6937,6 +7004,87 @@
|
||||||
],
|
],
|
||||||
"time": "2021-01-27T11:24:50+00:00"
|
"time": "2021-01-27T11:24:50+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/mailer",
|
||||||
|
"version": "v5.2.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/mailer.git",
|
||||||
|
"reference": "1efa11a8f59b8ba706aa6ee112c4675dce4dccf6"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/mailer/zipball/1efa11a8f59b8ba706aa6ee112c4675dce4dccf6",
|
||||||
|
"reference": "1efa11a8f59b8ba706aa6ee112c4675dce4dccf6",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"egulias/email-validator": "^2.1.10",
|
||||||
|
"php": ">=7.2.5",
|
||||||
|
"psr/log": "~1.0",
|
||||||
|
"symfony/event-dispatcher": "^4.4|^5.0",
|
||||||
|
"symfony/mime": "^5.2",
|
||||||
|
"symfony/polyfill-php80": "^1.15",
|
||||||
|
"symfony/service-contracts": "^1.1|^2"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/http-kernel": "<4.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/amazon-mailer": "^4.4|^5.0",
|
||||||
|
"symfony/google-mailer": "^4.4|^5.0",
|
||||||
|
"symfony/http-client-contracts": "^1.1|^2",
|
||||||
|
"symfony/mailchimp-mailer": "^4.4|^5.0",
|
||||||
|
"symfony/mailgun-mailer": "^4.4|^5.0",
|
||||||
|
"symfony/mailjet-mailer": "^4.4|^5.0",
|
||||||
|
"symfony/messenger": "^4.4|^5.0",
|
||||||
|
"symfony/postmark-mailer": "^4.4|^5.0",
|
||||||
|
"symfony/sendgrid-mailer": "^4.4|^5.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Mailer\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Helps sending emails",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/mailer/tree/v5.2.3"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2021-02-02T06:10:15+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/messenger",
|
"name": "symfony/messenger",
|
||||||
"version": "v5.2.3",
|
"version": "v5.2.3",
|
||||||
|
@ -7025,6 +7173,88 @@
|
||||||
],
|
],
|
||||||
"time": "2021-01-27T11:24:50+00:00"
|
"time": "2021-01-27T11:24:50+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/mime",
|
||||||
|
"version": "v5.2.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/mime.git",
|
||||||
|
"reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/mime/zipball/7dee6a43493f39b51ff6c5bb2bd576fe40a76c86",
|
||||||
|
"reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.2.5",
|
||||||
|
"symfony/deprecation-contracts": "^2.1",
|
||||||
|
"symfony/polyfill-intl-idn": "^1.10",
|
||||||
|
"symfony/polyfill-mbstring": "^1.0",
|
||||||
|
"symfony/polyfill-php80": "^1.15"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpdocumentor/reflection-docblock": "<3.2.2",
|
||||||
|
"phpdocumentor/type-resolver": "<1.4.0",
|
||||||
|
"symfony/mailer": "<4.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"egulias/email-validator": "^2.1.10",
|
||||||
|
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
||||||
|
"symfony/dependency-injection": "^4.4|^5.0",
|
||||||
|
"symfony/property-access": "^4.4|^5.1",
|
||||||
|
"symfony/property-info": "^4.4|^5.1",
|
||||||
|
"symfony/serializer": "^5.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Mime\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Allows manipulating MIME messages",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"mime",
|
||||||
|
"mime-type"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/mime/tree/v5.2.3"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2021-02-02T06:10:15+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.22.1",
|
"version": "v1.22.1",
|
||||||
|
@ -7104,6 +7334,93 @@
|
||||||
],
|
],
|
||||||
"time": "2021-01-07T16:49:33+00:00"
|
"time": "2021-01-07T16:49:33+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-intl-idn",
|
||||||
|
"version": "v1.22.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||||
|
"reference": "2d63434d922daf7da8dd863e7907e67ee3031483"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/2d63434d922daf7da8dd863e7907e67ee3031483",
|
||||||
|
"reference": "2d63434d922daf7da8dd863e7907e67ee3031483",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1",
|
||||||
|
"symfony/polyfill-intl-normalizer": "^1.10",
|
||||||
|
"symfony/polyfill-php72": "^1.10"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-intl": "For best performance"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "1.22-dev"
|
||||||
|
},
|
||||||
|
"thanks": {
|
||||||
|
"name": "symfony/polyfill",
|
||||||
|
"url": "https://github.com/symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Intl\\Idn\\": ""
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Laurent Bassin",
|
||||||
|
"email": "laurent@bassin.info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Trevor Rowbotham",
|
||||||
|
"email": "trevor.rowbotham@pm.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"compatibility",
|
||||||
|
"idn",
|
||||||
|
"intl",
|
||||||
|
"polyfill",
|
||||||
|
"portable",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2021-01-22T09:19:47+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php80",
|
"name": "symfony/polyfill-php80",
|
||||||
"version": "v1.22.1",
|
"version": "v1.22.1",
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Customization;
|
|
||||||
use App\Environment;
|
use App\Environment;
|
||||||
|
use App\Http\ServerRequest;
|
||||||
|
use App\Locale;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,7 +133,13 @@ return [
|
||||||
'inline' => [
|
'inline' => [
|
||||||
'js' => [
|
'js' => [
|
||||||
function (Request $request) {
|
function (Request $request) {
|
||||||
$locale = $request->getAttribute('locale', Customization::DEFAULT_LOCALE);
|
/** @var Locale|null $locale */
|
||||||
|
$localeObj = $request->getAttribute(ServerRequest::ATTR_LOCALE);
|
||||||
|
|
||||||
|
$locale = ($localeObj instanceof Locale)
|
||||||
|
? $localeObj->getLocale()
|
||||||
|
: Locale::DEFAULT_LOCALE;
|
||||||
|
|
||||||
$locale = explode('.', $locale)[0];
|
$locale = explode('.', $locale)[0];
|
||||||
$localeShort = substr($locale, 0, 2);
|
$localeShort = substr($locale, 0, 2);
|
||||||
$localeWithDashes = str_replace('_', '-', $locale);
|
$localeWithDashes = str_replace('_', '-', $locale);
|
||||||
|
|
|
@ -224,6 +224,95 @@ return [
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'mail' => [
|
||||||
|
'tab' => 'services',
|
||||||
|
'legend' => __('E-mail Delivery Service'),
|
||||||
|
'description' => __('Used for "Forgot Password" functionality, web hooks and other functions.'),
|
||||||
|
'use_grid' => true,
|
||||||
|
|
||||||
|
'elements' => [
|
||||||
|
|
||||||
|
'mailEnabled' => [
|
||||||
|
'toggle',
|
||||||
|
[
|
||||||
|
'label' => __('Enable Mail Delivery'),
|
||||||
|
'selected_text' => __('Yes'),
|
||||||
|
'deselected_text' => __('No'),
|
||||||
|
'default' => false,
|
||||||
|
'form_group_class' => 'col-md-12',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mailSenderName' => [
|
||||||
|
'text',
|
||||||
|
[
|
||||||
|
'label' => __('Sender Name'),
|
||||||
|
'default' => 'AzuraCast',
|
||||||
|
'form_group_class' => 'col-md-6',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mailSenderEmail' => [
|
||||||
|
'email',
|
||||||
|
[
|
||||||
|
'label' => __('Sender E-mail Address'),
|
||||||
|
'required' => false,
|
||||||
|
'default' => '',
|
||||||
|
'form_group_class' => 'col-md-6',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mailSmtpHost' => [
|
||||||
|
'text',
|
||||||
|
[
|
||||||
|
'label' => __('SMTP Host'),
|
||||||
|
'default' => '',
|
||||||
|
'form_group_class' => 'col-md-4',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mailSmtpPort' => [
|
||||||
|
'number',
|
||||||
|
[
|
||||||
|
'label' => __('SMTP Port'),
|
||||||
|
'default' => 465,
|
||||||
|
'form_group_class' => 'col-md-3',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mailSmtpSecure' => [
|
||||||
|
'toggle',
|
||||||
|
[
|
||||||
|
'label' => __('Use Secure (TLS) SMTP Connection'),
|
||||||
|
'description' => __('Usually enabled for port 465, disabled for ports 587 or 25.'),
|
||||||
|
|
||||||
|
'selected_text' => __('Yes'),
|
||||||
|
'deselected_text' => __('No'),
|
||||||
|
'default' => true,
|
||||||
|
'form_group_class' => 'col-md-5',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mailSmtpUsername' => [
|
||||||
|
'text',
|
||||||
|
[
|
||||||
|
'label' => __('SMTP Username'),
|
||||||
|
'default' => '',
|
||||||
|
'form_group_class' => 'col-md-6',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mailSmtpPassword' => [
|
||||||
|
'password',
|
||||||
|
[
|
||||||
|
'label' => __('SMTP Password'),
|
||||||
|
'default' => '',
|
||||||
|
'form_group_class' => 'col-md-6',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
'thirdPartyServices' => [
|
'thirdPartyServices' => [
|
||||||
'tab' => 'services',
|
'tab' => 'services',
|
||||||
'use_grid' => true,
|
'use_grid' => true,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use App\Message;
|
use App\Message;
|
||||||
use App\Radio\Backend\Liquidsoap;
|
use App\Radio\Backend\Liquidsoap;
|
||||||
use App\Sync\Task;
|
use App\Sync\Task;
|
||||||
|
use Symfony\Component\Mailer;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Message\AddNewMediaMessage::class => Task\CheckMediaTask::class,
|
Message\AddNewMediaMessage::class => Task\CheckMediaTask::class,
|
||||||
|
@ -17,4 +18,6 @@ return [
|
||||||
Message\RunSyncTaskMessage::class => App\Sync\Runner::class,
|
Message\RunSyncTaskMessage::class => App\Sync\Runner::class,
|
||||||
|
|
||||||
Message\DispatchWebhookMessage::class => App\Webhook\Dispatcher::class,
|
Message\DispatchWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||||
|
|
||||||
|
Mailer\Messenger\SendEmailMessage::class => Mailer\Messenger\MessageHandler::class,
|
||||||
];
|
];
|
||||||
|
|
|
@ -71,6 +71,14 @@ return function (App $app) {
|
||||||
->setName('account:login:2fa')
|
->setName('account:login:2fa')
|
||||||
->add(Middleware\EnableView::class);
|
->add(Middleware\EnableView::class);
|
||||||
|
|
||||||
|
$app->map(['GET', 'POST'], '/forgot', Controller\Frontend\Account\ForgotPasswordAction::class)
|
||||||
|
->setName('account:forgot')
|
||||||
|
->add(Middleware\EnableView::class);
|
||||||
|
|
||||||
|
$app->map(['GET', 'POST'], '/recover/{token}', Controller\Frontend\Account\RecoverAction::class)
|
||||||
|
->setName('account:recover')
|
||||||
|
->add(Middleware\EnableView::class);
|
||||||
|
|
||||||
$app->group(
|
$app->group(
|
||||||
'/setup',
|
'/setup',
|
||||||
function (RouteCollectorProxy $group) {
|
function (RouteCollectorProxy $group) {
|
||||||
|
|
|
@ -365,6 +365,73 @@ return [
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Symfony\Component\Messenger\MessageBusInterface::class => DI\get(
|
||||||
|
Symfony\Component\Messenger\MessageBus::class
|
||||||
|
),
|
||||||
|
|
||||||
|
// Mail functionality
|
||||||
|
Symfony\Component\Mailer\Transport\TransportInterface::class => function (
|
||||||
|
App\Entity\Repository\SettingsRepository $settingsRepo,
|
||||||
|
App\EventDispatcher $eventDispatcher,
|
||||||
|
Monolog\Logger $logger
|
||||||
|
) {
|
||||||
|
$settings = $settingsRepo->readSettings();
|
||||||
|
|
||||||
|
if ($settings->getMailEnabled()) {
|
||||||
|
$requiredSettings = [
|
||||||
|
'mailSenderEmail' => $settings->getMailSenderEmail(),
|
||||||
|
'mailSmtpHost' => $settings->getMailSmtpHost(),
|
||||||
|
'mailSmtpPort' => $settings->getMailSmtpPort(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasAllSettings = true;
|
||||||
|
foreach ($requiredSettings as $settingKey => $setting) {
|
||||||
|
if (empty($setting)) {
|
||||||
|
$hasAllSettings = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasAllSettings) {
|
||||||
|
$transport = new Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
|
||||||
|
$settings->getMailSmtpHost(),
|
||||||
|
$settings->getMailSmtpPort(),
|
||||||
|
$settings->getMailSmtpSecure(),
|
||||||
|
$eventDispatcher,
|
||||||
|
$logger
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($settings->getMailSmtpUsername())) {
|
||||||
|
$transport->setUsername($settings->getMailSmtpUsername());
|
||||||
|
$transport->setPassword($settings->getMailSmtpPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transport;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Symfony\Component\Mailer\Transport\NullTransport(
|
||||||
|
$eventDispatcher,
|
||||||
|
$logger
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
Symfony\Component\Mailer\Mailer::class => function (
|
||||||
|
Symfony\Component\Mailer\Transport\TransportInterface $transport,
|
||||||
|
Symfony\Component\Messenger\MessageBus $messageBus,
|
||||||
|
App\EventDispatcher $eventDispatcher
|
||||||
|
) {
|
||||||
|
return new Symfony\Component\Mailer\Mailer(
|
||||||
|
$transport,
|
||||||
|
$messageBus,
|
||||||
|
$eventDispatcher
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
Symfony\Component\Mailer\MailerInterface::class => DI\get(
|
||||||
|
Symfony\Component\Mailer\Mailer::class
|
||||||
|
),
|
||||||
|
|
||||||
// Supervisor manager
|
// Supervisor manager
|
||||||
Supervisor\Supervisor::class => function (Environment $settings, Psr\Log\LoggerInterface $logger) {
|
Supervisor\Supervisor::class => function (Environment $settings, Psr\Log\LoggerInterface $logger) {
|
||||||
$client = new fXmlRpc\Client(
|
$client = new fXmlRpc\Client(
|
||||||
|
|
|
@ -44,7 +44,7 @@ class Auth
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @param string $password
|
* @param string $password
|
||||||
*/
|
*/
|
||||||
public function authenticate($username, $password): ?User
|
public function authenticate(string $username, string $password): ?User
|
||||||
{
|
{
|
||||||
$user_auth = $this->userRepo->authenticate($username, $password);
|
$user_auth = $this->userRepo->authenticate($username, $password);
|
||||||
|
|
||||||
|
|
94
src/Controller/Frontend/Account/ForgotPasswordAction.php
Normal file
94
src/Controller/Frontend/Account/ForgotPasswordAction.php
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Frontend\Account;
|
||||||
|
|
||||||
|
use App\Entity;
|
||||||
|
use App\Exception\RateLimitExceededException;
|
||||||
|
use App\Http\Response;
|
||||||
|
use App\Http\ServerRequest;
|
||||||
|
use App\RateLimit;
|
||||||
|
use App\Session\Flash;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
|
class ForgotPasswordAction
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
ServerRequest $request,
|
||||||
|
Response $response,
|
||||||
|
Entity\Repository\SettingsRepository $settingsRepo,
|
||||||
|
Entity\Repository\UserRepository $userRepo,
|
||||||
|
Entity\Repository\UserLoginTokenRepository $loginTokenRepo,
|
||||||
|
RateLimit $rateLimit,
|
||||||
|
MailerInterface $mailer
|
||||||
|
): ResponseInterface {
|
||||||
|
$flash = $request->getFlash();
|
||||||
|
$view = $request->getView();
|
||||||
|
|
||||||
|
$settings = $settingsRepo->readSettings();
|
||||||
|
if (!$settings->getMailEnabled()) {
|
||||||
|
return $view->renderToResponse($response, 'frontend/account/forgot_disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isPost()) {
|
||||||
|
try {
|
||||||
|
$rateLimit->checkRequestRateLimit($request, 'forgot', 30, 3);
|
||||||
|
} catch (RateLimitExceededException $e) {
|
||||||
|
$flash->addMessage(
|
||||||
|
sprintf(
|
||||||
|
'<b>%s</b><br>%s',
|
||||||
|
__('Too many forgot password attempts'),
|
||||||
|
__(
|
||||||
|
'You have attempted to reset your password too many times. Please wait '
|
||||||
|
. '30 seconds and try again.'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Flash::ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response->withRedirect($request->getUri()->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $request->getParsedBodyParam('email', '');
|
||||||
|
$user = $userRepo->findByEmail($email);
|
||||||
|
|
||||||
|
if ($user instanceof Entity\User) {
|
||||||
|
$email = new Email();
|
||||||
|
$email->from(new Address($settings->getMailSenderEmail(), $settings->getMailSenderName()));
|
||||||
|
$email->to($user->getEmail());
|
||||||
|
|
||||||
|
$email->subject(__('Account Recovery Link'));
|
||||||
|
|
||||||
|
$loginToken = $loginTokenRepo->createToken($user);
|
||||||
|
$email->text(
|
||||||
|
$view->render(
|
||||||
|
'mail/forgot',
|
||||||
|
[
|
||||||
|
'token' => (string)$loginToken,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mailer->send($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash->addMessage(
|
||||||
|
sprintf(
|
||||||
|
'<b>%s</b><br>%s',
|
||||||
|
__('Account recovery e-mail sent.'),
|
||||||
|
__(
|
||||||
|
'If the e-mail address you provided is in the system, check your inbox '
|
||||||
|
. 'for a password reset message.'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Flash::SUCCESS
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response->withRedirect($request->getRouter()->named('home'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $view->renderToResponse($response, 'frontend/account/forgot');
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
namespace App\Controller\Frontend\Account;
|
namespace App\Controller\Frontend\Account;
|
||||||
|
|
||||||
use App\Entity\Repository\SettingsRepository;
|
use App\Entity;
|
||||||
use App\Entity\User;
|
|
||||||
use App\Exception\RateLimitExceededException;
|
use App\Exception\RateLimitExceededException;
|
||||||
use App\Http\Response;
|
use App\Http\Response;
|
||||||
use App\Http\ServerRequest;
|
use App\Http\ServerRequest;
|
||||||
|
@ -20,7 +19,8 @@ class LoginAction
|
||||||
Response $response,
|
Response $response,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
RateLimit $rateLimit,
|
RateLimit $rateLimit,
|
||||||
SettingsRepository $settingsRepo
|
Entity\Repository\SettingsRepository $settingsRepo,
|
||||||
|
Entity\Repository\UserLoginTokenRepository $loginTokenRepo
|
||||||
): ResponseInterface {
|
): ResponseInterface {
|
||||||
$auth = $request->getAuth();
|
$auth = $request->getAuth();
|
||||||
$acl = $request->getAcl();
|
$acl = $request->getAcl();
|
||||||
|
@ -64,7 +64,7 @@ class LoginAction
|
||||||
|
|
||||||
$user = $auth->authenticate($request->getParam('username'), $request->getParam('password'));
|
$user = $auth->authenticate($request->getParam('username'), $request->getParam('password'));
|
||||||
|
|
||||||
if ($user instanceof User) {
|
if ($user instanceof Entity\User) {
|
||||||
// If user selects "remember me", extend the cookie/session lifetime.
|
// If user selects "remember me", extend the cookie/session lifetime.
|
||||||
$session = $request->getSession();
|
$session = $request->getSession();
|
||||||
if ($session instanceof SessionCookiePersistenceInterface) {
|
if ($session instanceof SessionCookiePersistenceInterface) {
|
||||||
|
|
62
src/Controller/Frontend/Account/RecoverAction.php
Normal file
62
src/Controller/Frontend/Account/RecoverAction.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Frontend\Account;
|
||||||
|
|
||||||
|
use App\Entity;
|
||||||
|
use App\Http\Response;
|
||||||
|
use App\Http\ServerRequest;
|
||||||
|
use App\Session\Flash;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
class RecoverAction
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
ServerRequest $request,
|
||||||
|
Response $response,
|
||||||
|
string $token,
|
||||||
|
Entity\Repository\UserLoginTokenRepository $loginTokenRepo,
|
||||||
|
EntityManagerInterface $em
|
||||||
|
): ResponseInterface {
|
||||||
|
$flash = $request->getFlash();
|
||||||
|
|
||||||
|
$user = $loginTokenRepo->authenticate($token);
|
||||||
|
|
||||||
|
if (!$user instanceof Entity\User) {
|
||||||
|
$flash->addMessage(
|
||||||
|
sprintf(
|
||||||
|
'<b>%s</b>',
|
||||||
|
__('Invalid token specified.'),
|
||||||
|
),
|
||||||
|
Flash::ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response->withRedirect($request->getRouter()->named('account:login'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isPost()) {
|
||||||
|
$newPassword = $request->getParsedBodyParam('password');
|
||||||
|
|
||||||
|
$user->setNewPassword($newPassword);
|
||||||
|
$em->persist($user);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$request->getAuth()->setUser($user);
|
||||||
|
|
||||||
|
$loginTokenRepo->revokeForUser($user);
|
||||||
|
|
||||||
|
$flash->addMessage(
|
||||||
|
sprintf(
|
||||||
|
'<b>%s</b><br>%s',
|
||||||
|
__('Logged in using account recovery token'),
|
||||||
|
__('Your password has been updated.')
|
||||||
|
),
|
||||||
|
Flash::SUCCESS
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response->withRedirect($request->getRouter()->named('dashboard'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request->getView()->renderToResponse($response, 'frontend/account/recover');
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,13 +5,10 @@ namespace App;
|
||||||
use App\Entity;
|
use App\Entity;
|
||||||
use App\Http\ServerRequest;
|
use App\Http\ServerRequest;
|
||||||
use App\Service\NChan;
|
use App\Service\NChan;
|
||||||
use Gettext\Translator;
|
|
||||||
use Locale;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
class Customization
|
class Customization
|
||||||
{
|
{
|
||||||
public const DEFAULT_LOCALE = 'en_US.UTF-8';
|
|
||||||
public const DEFAULT_THEME = 'light';
|
public const DEFAULT_THEME = 'light';
|
||||||
|
|
||||||
public const THEME_LIGHT = 'light';
|
public const THEME_LIGHT = 'light';
|
||||||
|
@ -23,7 +20,7 @@ class Customization
|
||||||
|
|
||||||
protected Environment $environment;
|
protected Environment $environment;
|
||||||
|
|
||||||
protected string $locale = self::DEFAULT_LOCALE;
|
protected Locale $locale;
|
||||||
|
|
||||||
protected string $theme = self::DEFAULT_THEME;
|
protected string $theme = self::DEFAULT_THEME;
|
||||||
|
|
||||||
|
@ -44,8 +41,6 @@ class Customization
|
||||||
// Register current user
|
// Register current user
|
||||||
$this->user = $request->getAttribute(ServerRequest::ATTR_USER);
|
$this->user = $request->getAttribute(ServerRequest::ATTR_USER);
|
||||||
|
|
||||||
$this->locale = $this->initLocale($request);
|
|
||||||
|
|
||||||
// Register current theme
|
// Register current theme
|
||||||
$queryParams = $request->getQueryParams();
|
$queryParams = $request->getQueryParams();
|
||||||
|
|
||||||
|
@ -59,88 +54,16 @@ class Customization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the PHP translator
|
// Register locale
|
||||||
$translator = new Translator();
|
$this->locale = new Locale($environment, $request);
|
||||||
|
$this->locale->register();
|
||||||
$locale_base = $environment->getBaseDirectory() . '/resources/locale/compiled';
|
|
||||||
$locale_path = $locale_base . '/' . $this->locale . '.php';
|
|
||||||
|
|
||||||
if (file_exists($locale_path)) {
|
|
||||||
$translator->loadTranslations($locale_path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$translator->register();
|
public function getLocale(): Locale
|
||||||
|
|
||||||
// Register translation superglobal functions
|
|
||||||
setlocale(LC_ALL, $this->locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the user-customized, browser-specified or system default locale.
|
|
||||||
*
|
|
||||||
* @param ServerRequestInterface|null $request
|
|
||||||
*/
|
|
||||||
protected function initLocale(?ServerRequestInterface $request = null): string
|
|
||||||
{
|
|
||||||
$supported_locales = $this->environment->getSupportedLocales();
|
|
||||||
$try_locales = [];
|
|
||||||
|
|
||||||
// Prefer user-based profile locale.
|
|
||||||
if ($this->user !== null && !empty($this->user->getLocale()) && 'default' !== $this->user->getLocale()) {
|
|
||||||
$try_locales[] = $this->user->getLocale();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to load from browser headers.
|
|
||||||
if ($request instanceof ServerRequestInterface) {
|
|
||||||
$server_params = $request->getServerParams();
|
|
||||||
$browser_locale = Locale::acceptFromHttp($server_params['HTTP_ACCEPT_LANGUAGE'] ?? null);
|
|
||||||
|
|
||||||
if (!empty($browser_locale)) {
|
|
||||||
if (2 === strlen($browser_locale)) {
|
|
||||||
$browser_locale = strtolower($browser_locale) . '_' . strtoupper($browser_locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
$try_locales[] = substr($browser_locale, 0, 5) . '.UTF-8';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to load from environment variable.
|
|
||||||
$envLocale = $this->environment->getLang();
|
|
||||||
if (!empty($envLocale)) {
|
|
||||||
$try_locales[] = substr($envLocale, 0, 5) . '.UTF-8';
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($try_locales as $exact_locale) {
|
|
||||||
// Prefer exact match.
|
|
||||||
if (isset($supported_locales[$exact_locale])) {
|
|
||||||
return $exact_locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use approximate match if available.
|
|
||||||
foreach ($supported_locales as $lang_code => $lang_name) {
|
|
||||||
if (strpos($exact_locale, substr($lang_code, 0, 2)) === 0) {
|
|
||||||
return $lang_code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to system option.
|
|
||||||
return self::DEFAULT_LOCALE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLocale(): string
|
|
||||||
{
|
{
|
||||||
return $this->locale;
|
return $this->locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string A shortened locale (minus .UTF-8) for use in Vue.
|
|
||||||
*/
|
|
||||||
public function getVueLocale(): string
|
|
||||||
{
|
|
||||||
return json_encode(substr($this->getLocale(), 0, 5), JSON_THROW_ON_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the user-customized or system default theme.
|
* Returns the user-customized or system default theme.
|
||||||
*/
|
*/
|
||||||
|
@ -235,13 +158,4 @@ class Customization
|
||||||
|
|
||||||
return $this->settings->getEnableWebsockets();
|
return $this->settings->getEnableWebsockets();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the CLI without instantiating the Doctrine DB stack (allowing cache clearing, etc.).
|
|
||||||
*/
|
|
||||||
public static function initCli(): void
|
|
||||||
{
|
|
||||||
$translator = new Translator();
|
|
||||||
$translator->register();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,24 +17,9 @@ use JsonSerializable;
|
||||||
*/
|
*/
|
||||||
class ApiKey implements JsonSerializable
|
class ApiKey implements JsonSerializable
|
||||||
{
|
{
|
||||||
|
use Traits\HasSplitTokenFields;
|
||||||
use Traits\TruncateStrings;
|
use Traits\TruncateStrings;
|
||||||
|
|
||||||
/**
|
|
||||||
* @ORM\Column(name="id", type="string", length=16)
|
|
||||||
* @ORM\Id
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ORM\Column(name="verifier", type="string", length=128, nullable=false)
|
|
||||||
*
|
|
||||||
* @AuditLog\AuditIgnore()
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $verifier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity="User", inversedBy="api_keys", fetch="EAGER")
|
* @ORM\ManyToOne(targetEntity="User", inversedBy="api_keys", fetch="EAGER")
|
||||||
* @ORM\JoinColumns({
|
* @ORM\JoinColumns({
|
||||||
|
@ -53,13 +38,7 @@ class ApiKey implements JsonSerializable
|
||||||
public function __construct(User $user, SplitToken $token)
|
public function __construct(User $user, SplitToken $token)
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
$this->id = $token->identifier;
|
$this->setFromToken($token);
|
||||||
$this->verifier = $token->hashVerifier();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): string
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUser(): User
|
public function getUser(): User
|
||||||
|
@ -67,17 +46,6 @@ class ApiKey implements JsonSerializable
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify an incoming API key against the verifier on this record.
|
|
||||||
*
|
|
||||||
* @param SplitToken $userSuppliedToken
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public function verify(SplitToken $userSuppliedToken): bool
|
|
||||||
{
|
|
||||||
return $userSuppliedToken->verify($this->verifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @AuditLog\AuditIdentifier
|
* @AuditLog\AuditIdentifier
|
||||||
*/
|
*/
|
||||||
|
|
32
src/Entity/Migration/Version20210226053617.php
Normal file
32
src/Entity/Migration/Version20210226053617.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity\Migration;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20210226053617 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create the user login token table.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE user_login_tokens (id VARCHAR(16) NOT NULL, user_id INT DEFAULT NULL, created_at INT NOT NULL, verifier VARCHAR(128) NOT NULL, INDEX IDX_DDF24A16A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_general_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE user_login_tokens ADD CONSTRAINT FK_DDF24A16A76ED395 FOREIGN KEY (user_id) REFERENCES users (uid) ON DELETE CASCADE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP TABLE user_login_tokens');
|
||||||
|
}
|
||||||
|
}
|
30
src/Entity/Repository/AbstractSplitTokenRepository.php
Normal file
30
src/Entity/Repository/AbstractSplitTokenRepository.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity\Repository;
|
||||||
|
|
||||||
|
use App\Doctrine\Repository;
|
||||||
|
use App\Entity;
|
||||||
|
use App\Security\SplitToken;
|
||||||
|
|
||||||
|
abstract class AbstractSplitTokenRepository extends Repository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Given an API key string in the format `identifier:verifier`, find and authenticate an API key.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*/
|
||||||
|
public function authenticate(string $key): ?Entity\User
|
||||||
|
{
|
||||||
|
$userSuppliedToken = SplitToken::fromKeyString($key);
|
||||||
|
|
||||||
|
$tokenEntity = $this->repository->find($userSuppliedToken->identifier);
|
||||||
|
|
||||||
|
if ($tokenEntity instanceof $this->entityClass) {
|
||||||
|
return ($tokenEntity->verify($userSuppliedToken))
|
||||||
|
? $tokenEntity->getUser()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,29 +2,6 @@
|
||||||
|
|
||||||
namespace App\Entity\Repository;
|
namespace App\Entity\Repository;
|
||||||
|
|
||||||
use App\Doctrine\Repository;
|
class ApiKeyRepository extends AbstractSplitTokenRepository
|
||||||
use App\Entity;
|
|
||||||
use App\Security\SplitToken;
|
|
||||||
|
|
||||||
class ApiKeyRepository extends Repository
|
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Given an API key string in the format `identifier:verifier`, find and authenticate an API key.
|
|
||||||
*
|
|
||||||
* @param string $key
|
|
||||||
*/
|
|
||||||
public function authenticate(string $key): ?Entity\User
|
|
||||||
{
|
|
||||||
$userSuppliedToken = SplitToken::fromKeyString($key);
|
|
||||||
|
|
||||||
$api_key = $this->repository->find($userSuppliedToken->identifier);
|
|
||||||
|
|
||||||
if ($api_key instanceof Entity\ApiKey) {
|
|
||||||
return ($api_key->verify($userSuppliedToken))
|
|
||||||
? $api_key->getUser()
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
43
src/Entity/Repository/UserLoginTokenRepository.php
Normal file
43
src/Entity/Repository/UserLoginTokenRepository.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity\Repository;
|
||||||
|
|
||||||
|
use App\Entity;
|
||||||
|
use App\Security\SplitToken;
|
||||||
|
|
||||||
|
class UserLoginTokenRepository extends AbstractSplitTokenRepository
|
||||||
|
{
|
||||||
|
public function createToken(Entity\User $user): SplitToken
|
||||||
|
{
|
||||||
|
$token = SplitToken::generate();
|
||||||
|
|
||||||
|
$loginToken = new Entity\UserLoginToken($user, $token);
|
||||||
|
$this->em->persist($loginToken);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revokeForUser(Entity\User $user): void
|
||||||
|
{
|
||||||
|
$this->em->createQuery(
|
||||||
|
<<<'DQL'
|
||||||
|
DELETE FROM App\Entity\UserLoginToken ult
|
||||||
|
WHERE ult.user = :user
|
||||||
|
DQL
|
||||||
|
)->setParameter('user', $user)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cleanup(): void
|
||||||
|
{
|
||||||
|
$threshold = time() - 86400; // One day
|
||||||
|
|
||||||
|
$this->em->createQuery(
|
||||||
|
<<<'DQL'
|
||||||
|
DELETE FROM App\Entity\UserLoginToken ut WHERE ut.created_at <= :threshold
|
||||||
|
DQL
|
||||||
|
)->setParameter('threshold', $threshold)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,35 +7,36 @@ use App\Entity;
|
||||||
|
|
||||||
class UserRepository extends Repository
|
class UserRepository extends Repository
|
||||||
{
|
{
|
||||||
/**
|
public function find(int $id): ?Entity\User
|
||||||
* @param string $username
|
|
||||||
* @param string $password
|
|
||||||
*
|
|
||||||
* @return bool|null|object
|
|
||||||
*/
|
|
||||||
public function authenticate($username, $password)
|
|
||||||
{
|
{
|
||||||
$login_info = $this->repository->findOneBy(['email' => $username]);
|
return $this->repository->find($id);
|
||||||
|
|
||||||
if (!($login_info instanceof Entity\User)) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($login_info->verifyPassword($password)) {
|
public function findByEmail(string $email): ?Entity\User
|
||||||
return $login_info;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates or returns an existing user with the specified e-mail address.
|
|
||||||
*
|
|
||||||
* @param string $email
|
|
||||||
*/
|
|
||||||
public function getOrCreate($email): Entity\User
|
|
||||||
{
|
{
|
||||||
$user = $this->repository->findOneBy(['email' => $email]);
|
return $this->repository->findOneby(['email' => $email]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(string $username, string $password): ?Entity\User
|
||||||
|
{
|
||||||
|
$user = $this->findByEmail($username);
|
||||||
|
|
||||||
|
if ($user instanceof Entity\User && $user->verifyPassword($password)) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify a password (and do nothing with it) to avoid timing attacks on authentication.
|
||||||
|
password_verify(
|
||||||
|
$password,
|
||||||
|
'$argon2id$v=19$m=65536,t=4,p=1$WHptOW0xM1UweHp0ZXpmNg$qC5anR37sV/G8k7l09eLKLHukkUD7e5csUdbmjGYsgs'
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrCreate(string $email): Entity\User
|
||||||
|
{
|
||||||
|
$user = $this->findByEmail($email);
|
||||||
if (!($user instanceof Entity\User)) {
|
if (!($user instanceof Entity\User)) {
|
||||||
$user = new Entity\User();
|
$user = new Entity\User();
|
||||||
$user->setEmail($email);
|
$user->setEmail($email);
|
||||||
|
|
|
@ -784,4 +784,132 @@ class Settings
|
||||||
{
|
{
|
||||||
$this->enableAdvancedFeatures = $enableAdvancedFeatures;
|
$this->enableAdvancedFeatures = $enableAdvancedFeatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example="true")
|
||||||
|
* @var bool Enable e-mail delivery across the application.
|
||||||
|
*/
|
||||||
|
protected bool $mailEnabled = false;
|
||||||
|
|
||||||
|
public function getMailEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->mailEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailEnabled(bool $mailEnabled): void
|
||||||
|
{
|
||||||
|
$this->mailEnabled = $mailEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example="AzuraCast")
|
||||||
|
* @var string The name of the sender of system e-mails.
|
||||||
|
*/
|
||||||
|
protected string $mailSenderName = '';
|
||||||
|
|
||||||
|
public function getMailSenderName(): string
|
||||||
|
{
|
||||||
|
return $this->mailSenderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailSenderName(string $mailSenderName): void
|
||||||
|
{
|
||||||
|
$this->mailSenderName = $mailSenderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example="example@example.com")
|
||||||
|
* @var string The e-mail address of the sender of system e-mails.
|
||||||
|
*/
|
||||||
|
protected string $mailSenderEmail = '';
|
||||||
|
|
||||||
|
public function getMailSenderEmail(): string
|
||||||
|
{
|
||||||
|
return $this->mailSenderEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailSenderEmail(string $mailSenderEmail): void
|
||||||
|
{
|
||||||
|
$this->mailSenderEmail = $mailSenderEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example="smtp.example.com")
|
||||||
|
* @var string The host to send outbound SMTP mail.
|
||||||
|
*/
|
||||||
|
protected string $mailSmtpHost = '';
|
||||||
|
|
||||||
|
public function getMailSmtpHost(): string
|
||||||
|
{
|
||||||
|
return $this->mailSmtpHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailSmtpHost(string $mailSmtpHost): void
|
||||||
|
{
|
||||||
|
$this->mailSmtpHost = $mailSmtpHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example=465)
|
||||||
|
* @var int The port for sending outbound SMTP mail.
|
||||||
|
*/
|
||||||
|
protected int $mailSmtpPort = 0;
|
||||||
|
|
||||||
|
public function getMailSmtpPort(): int
|
||||||
|
{
|
||||||
|
return $this->mailSmtpPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailSmtpPort(int $mailSmtpPort): void
|
||||||
|
{
|
||||||
|
$this->mailSmtpPort = $mailSmtpPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example="username")
|
||||||
|
* @var string The username when connecting to SMTP mail.
|
||||||
|
*/
|
||||||
|
protected string $mailSmtpUsername = '';
|
||||||
|
|
||||||
|
public function getMailSmtpUsername(): string
|
||||||
|
{
|
||||||
|
return $this->mailSmtpUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailSmtpUsername(string $mailSmtpUsername): void
|
||||||
|
{
|
||||||
|
$this->mailSmtpUsername = $mailSmtpUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example="password")
|
||||||
|
* @var string The password when connecting to SMTP mail.
|
||||||
|
*/
|
||||||
|
protected string $mailSmtpPassword = '';
|
||||||
|
|
||||||
|
public function getMailSmtpPassword(): string
|
||||||
|
{
|
||||||
|
return $this->mailSmtpPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailSmtpPassword(string $mailSmtpPassword): void
|
||||||
|
{
|
||||||
|
$this->mailSmtpPassword = $mailSmtpPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Property(example="true")
|
||||||
|
* @var bool Whether to use a secure (TLS) connection when sending SMTP mail.
|
||||||
|
*/
|
||||||
|
protected bool $mailSmtpSecure = true;
|
||||||
|
|
||||||
|
public function getMailSmtpSecure(): bool
|
||||||
|
{
|
||||||
|
return $this->mailSmtpSecure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMailSmtpSecure(bool $mailSmtpSecure): void
|
||||||
|
{
|
||||||
|
$this->mailSmtpSecure = $mailSmtpSecure;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
48
src/Entity/Traits/HasSplitTokenFields.php
Normal file
48
src/Entity/Traits/HasSplitTokenFields.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity\Traits;
|
||||||
|
|
||||||
|
use App\Annotations\AuditLog;
|
||||||
|
use App\Security\SplitToken;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
trait HasSplitTokenFields
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="id", type="string", length=16)
|
||||||
|
* @ORM\Id
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="verifier", type="string", length=128, nullable=false)
|
||||||
|
*
|
||||||
|
* @AuditLog\AuditIgnore()
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $verifier;
|
||||||
|
|
||||||
|
protected function setFromToken(SplitToken $token): void
|
||||||
|
{
|
||||||
|
$this->id = $token->identifier;
|
||||||
|
$this->verifier = $token->hashVerifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify an incoming API key against the verifier on this record.
|
||||||
|
*
|
||||||
|
* @param SplitToken $userSuppliedToken
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function verify(SplitToken $userSuppliedToken): bool
|
||||||
|
{
|
||||||
|
return $userSuppliedToken->verify($this->verifier);
|
||||||
|
}
|
||||||
|
}
|
44
src/Entity/UserLoginToken.php
Normal file
44
src/Entity/UserLoginToken.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @noinspection PhpMissingFieldTypeInspection */
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Security\SplitToken;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Table(name="user_login_tokens")
|
||||||
|
* @ORM\Entity(readOnly=true)
|
||||||
|
*/
|
||||||
|
class UserLoginToken
|
||||||
|
{
|
||||||
|
use Traits\HasSplitTokenFields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity="User", inversedBy="api_keys", fetch="EAGER")
|
||||||
|
* @ORM\JoinColumns({
|
||||||
|
* @ORM\JoinColumn(name="user_id", referencedColumnName="uid", onDelete="CASCADE")
|
||||||
|
* })
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
protected $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="created_at", type="integer")
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $created_at;
|
||||||
|
|
||||||
|
public function __construct(User $user, SplitToken $token)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
$this->setFromToken($token);
|
||||||
|
$this->created_at = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ use App\Auth;
|
||||||
use App\Customization;
|
use App\Customization;
|
||||||
use App\Entity;
|
use App\Entity;
|
||||||
use App\Exception;
|
use App\Exception;
|
||||||
|
use App\Locale;
|
||||||
use App\Radio;
|
use App\Radio;
|
||||||
use App\RateLimit;
|
use App\RateLimit;
|
||||||
use App\Session;
|
use App\Session;
|
||||||
|
@ -22,6 +23,7 @@ final class ServerRequest extends \Slim\Http\ServerRequest
|
||||||
public const ATTR_ROUTER = 'app_router';
|
public const ATTR_ROUTER = 'app_router';
|
||||||
public const ATTR_RATE_LIMIT = 'app_rate_limit';
|
public const ATTR_RATE_LIMIT = 'app_rate_limit';
|
||||||
public const ATTR_ACL = 'acl';
|
public const ATTR_ACL = 'acl';
|
||||||
|
public const ATTR_LOCALE = 'locale';
|
||||||
public const ATTR_CUSTOMIZATION = 'customization';
|
public const ATTR_CUSTOMIZATION = 'customization';
|
||||||
public const ATTR_AUTH = 'auth';
|
public const ATTR_AUTH = 'auth';
|
||||||
public const ATTR_STATION = 'station';
|
public const ATTR_STATION = 'station';
|
||||||
|
@ -60,6 +62,11 @@ final class ServerRequest extends \Slim\Http\ServerRequest
|
||||||
return $this->getAttributeOfClass(self::ATTR_RATE_LIMIT, RateLimit::class);
|
return $this->getAttributeOfClass(self::ATTR_RATE_LIMIT, RateLimit::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLocale(): Locale
|
||||||
|
{
|
||||||
|
return $this->getAttributeOfClass(self::ATTR_LOCALE, Locale::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function getCustomization(): Customization
|
public function getCustomization(): Customization
|
||||||
{
|
{
|
||||||
return $this->getAttributeOfClass(self::ATTR_CUSTOMIZATION, Customization::class);
|
return $this->getAttributeOfClass(self::ATTR_CUSTOMIZATION, Customization::class);
|
||||||
|
|
116
src/Locale.php
Normal file
116
src/Locale.php
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use App\Http\ServerRequest;
|
||||||
|
use Gettext\Translator;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
class Locale
|
||||||
|
{
|
||||||
|
public const DEFAULT_LOCALE = 'en_US.UTF-8';
|
||||||
|
|
||||||
|
protected Environment $environment;
|
||||||
|
|
||||||
|
protected ?ServerRequestInterface $request = null;
|
||||||
|
|
||||||
|
protected string $locale = self::DEFAULT_LOCALE;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Environment $environment,
|
||||||
|
?ServerRequestInterface $request = null
|
||||||
|
) {
|
||||||
|
$this->environment = $environment;
|
||||||
|
$this->request = $request;
|
||||||
|
|
||||||
|
$this->locale = $this->determineLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function determineLocale(): string
|
||||||
|
{
|
||||||
|
$possibleLocales = [];
|
||||||
|
|
||||||
|
// Attempt to load from request if provided.
|
||||||
|
if ($this->request instanceof ServerRequestInterface) {
|
||||||
|
// Prefer user-based profile locale.
|
||||||
|
$user = $this->request->getAttribute(ServerRequest::ATTR_USER);
|
||||||
|
if (null !== $user && !empty($user->getLocale()) && 'default' !== $user->getLocale()) {
|
||||||
|
$possibleLocales[] = $user->getLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
$server_params = $this->request->getServerParams();
|
||||||
|
$browser_locale = \Locale::acceptFromHttp($server_params['HTTP_ACCEPT_LANGUAGE'] ?? null);
|
||||||
|
|
||||||
|
if (!empty($browser_locale)) {
|
||||||
|
if (2 === strlen($browser_locale)) {
|
||||||
|
$browser_locale = strtolower($browser_locale) . '_' . strtoupper($browser_locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
$possibleLocales[] = substr($browser_locale, 0, 5) . '.UTF-8';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to load from environment variable.
|
||||||
|
$envLocale = $this->environment->getLang();
|
||||||
|
if (!empty($envLocale)) {
|
||||||
|
$possibleLocales[] = substr($envLocale, 0, 5) . '.UTF-8';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getValidLocale($possibleLocales);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getValidLocale(array $possibleLocales): string
|
||||||
|
{
|
||||||
|
$supportedLocales = $this->environment->getSupportedLocales();
|
||||||
|
|
||||||
|
foreach ($possibleLocales as $locale) {
|
||||||
|
// Prefer exact match.
|
||||||
|
if (isset($supportedLocales[$locale])) {
|
||||||
|
return $locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use approximate match if available.
|
||||||
|
foreach ($supportedLocales as $langCode => $langName) {
|
||||||
|
if (strpos($locale, substr($langCode, 0, 2)) === 0) {
|
||||||
|
return $langCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocale(): string
|
||||||
|
{
|
||||||
|
return $this->locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string A shortened locale (minus .UTF-8) for use in Vue.
|
||||||
|
*/
|
||||||
|
public function getVueLocale(): string
|
||||||
|
{
|
||||||
|
return json_encode(substr($this->locale, 0, 5), JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocale(string $newLocale = self::DEFAULT_LOCALE): void
|
||||||
|
{
|
||||||
|
$this->locale = $newLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$translator = new Translator();
|
||||||
|
|
||||||
|
$localeBase = $this->environment->getBaseDirectory() . '/resources/locale/compiled';
|
||||||
|
$localePath = $localeBase . '/' . $this->locale . '.php';
|
||||||
|
if (file_exists($localePath)) {
|
||||||
|
$translator->loadTranslations($localePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$translator->register();
|
||||||
|
|
||||||
|
// Register translation superglobal functions
|
||||||
|
setlocale(LC_ALL, $this->locale);
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,9 @@ class QueueManager implements SendersLocatorInterface
|
||||||
$message = $envelope->getMessage();
|
$message = $envelope->getMessage();
|
||||||
|
|
||||||
if (!$message instanceof AbstractMessage) {
|
if (!$message instanceof AbstractMessage) {
|
||||||
return [];
|
return [
|
||||||
|
$this->getTransport(self::QUEUE_NORMAL_PRIORITY),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$queue = $message->getQueue();
|
$queue = $message->getQueue();
|
||||||
|
|
|
@ -42,6 +42,8 @@ class GetCurrentUser implements MiddlewareInterface
|
||||||
->withAttribute('is_logged_in', (null !== $user));
|
->withAttribute('is_logged_in', (null !== $user));
|
||||||
|
|
||||||
// Initialize Customization (timezones, locales, etc) based on the current logged in user.
|
// Initialize Customization (timezones, locales, etc) based on the current logged in user.
|
||||||
|
|
||||||
|
/** @var Customization $customization */
|
||||||
$customization = $this->factory->make(
|
$customization = $this->factory->make(
|
||||||
Customization::class,
|
Customization::class,
|
||||||
[
|
[
|
||||||
|
@ -50,6 +52,8 @@ class GetCurrentUser implements MiddlewareInterface
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize ACL (can only be initialized after Customization as it contains localizations).
|
// Initialize ACL (can only be initialized after Customization as it contains localizations).
|
||||||
|
|
||||||
|
/** @var Acl $acl */
|
||||||
$acl = $this->factory->make(
|
$acl = $this->factory->make(
|
||||||
Acl::class,
|
Acl::class,
|
||||||
[
|
[
|
||||||
|
@ -58,7 +62,7 @@ class GetCurrentUser implements MiddlewareInterface
|
||||||
);
|
);
|
||||||
|
|
||||||
$request = $request
|
$request = $request
|
||||||
->withAttribute('locale', $customization->getLocale())
|
->withAttribute(ServerRequest::ATTR_LOCALE, $customization->getLocale())
|
||||||
->withAttribute(ServerRequest::ATTR_CUSTOMIZATION, $customization)
|
->withAttribute(ServerRequest::ATTR_CUSTOMIZATION, $customization)
|
||||||
->withAttribute(ServerRequest::ATTR_ACL, $acl);
|
->withAttribute(ServerRequest::ATTR_ACL, $acl);
|
||||||
|
|
||||||
|
|
27
src/Sync/Task/CleanupLoginTokensTask.php
Normal file
27
src/Sync/Task/CleanupLoginTokensTask.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Sync\Task;
|
||||||
|
|
||||||
|
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||||
|
use App\Entity;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class CleanupLoginTokensTask extends AbstractTask
|
||||||
|
{
|
||||||
|
protected Entity\Repository\UserLoginTokenRepository $loginTokenRepo;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
ReloadableEntityManagerInterface $em,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
Entity\Repository\UserLoginTokenRepository $loginTokenRepo
|
||||||
|
) {
|
||||||
|
parent::__construct($em, $logger);
|
||||||
|
|
||||||
|
$this->loginTokenRepo = $loginTokenRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(bool $force = false): void
|
||||||
|
{
|
||||||
|
$this->loginTokenRepo->cleanup();
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ class TaskLocator
|
||||||
GetSyncTasks::SYNC_LONG => [
|
GetSyncTasks::SYNC_LONG => [
|
||||||
Task\RunAnalyticsTask::class,
|
Task\RunAnalyticsTask::class,
|
||||||
Task\RunAutomatedAssignmentTask::class,
|
Task\RunAutomatedAssignmentTask::class,
|
||||||
|
Task\CleanupLoginTokensTask::class,
|
||||||
Task\CleanupHistoryTask::class,
|
Task\CleanupHistoryTask::class,
|
||||||
Task\CleanupStorageTask::class,
|
Task\CleanupStorageTask::class,
|
||||||
Task\RotateLogsTask::class,
|
Task\RotateLogsTask::class,
|
||||||
|
|
33
src/View.php
33
src/View.php
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
|
use App\Http\Router;
|
||||||
use App\Http\ServerRequest;
|
use App\Http\ServerRequest;
|
||||||
use DI\FactoryInterface;
|
use DI\FactoryInterface;
|
||||||
use Doctrine\Inflector\InflectorFactory;
|
use Doctrine\Inflector\InflectorFactory;
|
||||||
|
@ -16,24 +17,18 @@ class View extends Engine
|
||||||
{
|
{
|
||||||
protected Assets $assets;
|
protected Assets $assets;
|
||||||
|
|
||||||
|
protected ?ServerRequestInterface $request = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
FactoryInterface $factory,
|
FactoryInterface $factory,
|
||||||
Environment $environment,
|
Environment $environment,
|
||||||
EventDispatcher $dispatcher,
|
EventDispatcher $dispatcher,
|
||||||
Version $version,
|
Version $version,
|
||||||
ServerRequestInterface $request
|
?ServerRequestInterface $request = null
|
||||||
) {
|
) {
|
||||||
parent::__construct($environment->getViewsDirectory(), 'phtml');
|
parent::__construct($environment->getViewsDirectory(), 'phtml');
|
||||||
|
|
||||||
// Add non-request-dependent content.
|
// Add non-request-dependent content.
|
||||||
$this->addData(
|
|
||||||
[
|
|
||||||
'environment' => $environment,
|
|
||||||
'version' => $version,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add request-dependent content.
|
|
||||||
$this->assets = $factory->make(
|
$this->assets = $factory->make(
|
||||||
Assets::class,
|
Assets::class,
|
||||||
[
|
[
|
||||||
|
@ -41,6 +36,18 @@ class View extends Engine
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->addData(
|
||||||
|
[
|
||||||
|
'environment' => $environment,
|
||||||
|
'version' => $version,
|
||||||
|
'assets' => $this->assets,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add request-dependent content.
|
||||||
|
$this->request = $request;
|
||||||
|
|
||||||
|
if ($request instanceof ServerRequestInterface) {
|
||||||
$this->addData(
|
$this->addData(
|
||||||
[
|
[
|
||||||
'request' => $request,
|
'request' => $request,
|
||||||
|
@ -50,9 +57,15 @@ class View extends Engine
|
||||||
'customization' => $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION),
|
'customization' => $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION),
|
||||||
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
|
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
|
||||||
'user' => $request->getAttribute(ServerRequest::ATTR_USER),
|
'user' => $request->getAttribute(ServerRequest::ATTR_USER),
|
||||||
'assets' => $this->assets,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
$this->addData(
|
||||||
|
[
|
||||||
|
'router' => $factory->make(Router::class),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->registerFunction(
|
$this->registerFunction(
|
||||||
'escapeJs',
|
'escapeJs',
|
||||||
|
|
35
templates/frontend/account/forgot.phtml
Normal file
35
templates/frontend/account/forgot.phtml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$this->layout(
|
||||||
|
'minimal',
|
||||||
|
[
|
||||||
|
'title' => __('Forgot Password'),
|
||||||
|
'page_class' => 'login-content',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="public-page">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title mb-4 text-center"><?=__('Forgot Password')?></h2>
|
||||||
|
|
||||||
|
<form id="login-form" action="" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="mb-2">
|
||||||
|
<i class="material-icons mr-1" aria-hidden="true">email</i>
|
||||||
|
<strong><?=__('E-mail Address')?></strong>
|
||||||
|
</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" placeholder="<?=__(
|
||||||
|
'name@example.com'
|
||||||
|
)?>" aria-label="<?=__('E-mail Address')?>" required autofocus>
|
||||||
|
</div>
|
||||||
|
<button type="submit" role="button" title="<?=__(
|
||||||
|
'Sign in'
|
||||||
|
)?>" class="btn btn-login btn-primary btn-block mt-2 mb-3">
|
||||||
|
<?=__('Send Recovery E-mail')?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
35
templates/frontend/account/forgot_disabled.phtml
Normal file
35
templates/frontend/account/forgot_disabled.phtml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$this->layout(
|
||||||
|
'minimal',
|
||||||
|
[
|
||||||
|
'title' => __('Forgot Password'),
|
||||||
|
'page_class' => 'login-content',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="public-page">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-sm">
|
||||||
|
<h2 class="card-title mb-0 text-center"><?=__('Forgot Password')?></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<?=__('This installation\'s administrator has not configured this functionality.')?>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<?=__(
|
||||||
|
'Contact an administrator to reset your password following the instructions in our documentation:'
|
||||||
|
)?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a class="btn btn-primary btn-block mt-2 mb-3" href="https://docs.azuracast.com/en/administration/users#resetting-an-account-password" target="_blank">
|
||||||
|
<?=__('Password Reset Instructions')?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -40,7 +40,7 @@ $this->layout(
|
||||||
<i class="material-icons mr-1" aria-hidden="true">email</i>
|
<i class="material-icons mr-1" aria-hidden="true">email</i>
|
||||||
<strong><?=__('E-mail Address')?></strong>
|
<strong><?=__('E-mail Address')?></strong>
|
||||||
</label>
|
</label>
|
||||||
<input type="email" name="username" class="form-control" placeholder="<?=__(
|
<input type="email" id="username" name="username" class="form-control" placeholder="<?=__(
|
||||||
'name@example.com'
|
'name@example.com'
|
||||||
)?>" aria-label="<?=__('E-mail Address')?>" required autofocus>
|
)?>" aria-label="<?=__('E-mail Address')?>" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,7 +49,7 @@ $this->layout(
|
||||||
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
||||||
<strong><?=__('Password')?></strong>
|
<strong><?=__('Password')?></strong>
|
||||||
</label>
|
</label>
|
||||||
<input type="password" name="password" class="form-control" placeholder="<?=__(
|
<input type="password" id="password" name="password" class="form-control" placeholder="<?=__(
|
||||||
'Enter your password'
|
'Enter your password'
|
||||||
)?>" aria-label="<?=__('Password')?>" required>
|
)?>" aria-label="<?=__('Password')?>" required>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,8 +69,9 @@ $this->layout(
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="text-center"><?=__('Please log in to continue.')?> <?=sprintf(
|
<p class="text-center"><?=__('Please log in to continue.')?> <?=sprintf(
|
||||||
__('<a href="%s" target="_blank">Forgot your password?</a>'),
|
'<a href="%s">%s</a>',
|
||||||
'https://docs.azuracast.com/en/administration/users#resetting-an-account-password'
|
$router->named('account:forgot'),
|
||||||
|
__('Forgot your password?')
|
||||||
)?></p>
|
)?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
39
templates/frontend/account/recover.phtml
Normal file
39
templates/frontend/account/recover.phtml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$this->layout(
|
||||||
|
'minimal',
|
||||||
|
[
|
||||||
|
'title' => __('Recover Account'),
|
||||||
|
'page_class' => 'login-content',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var \App\Assets $assets */
|
||||||
|
$assets->load('zxcvbn');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="public-page">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title mb-2 text-center"><?=__('Recover Account')?></h2>
|
||||||
|
<p class="text-center mb-3"><?=__('Choose a new password for your account.')?></p>
|
||||||
|
|
||||||
|
<form id="login-form" action="" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="mb-2">
|
||||||
|
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
||||||
|
<strong><?=__('Password')?></strong>
|
||||||
|
</label>
|
||||||
|
<input type="password" id="password" name="password" class="strength form-control" placeholder="<?=__(
|
||||||
|
'Enter your password'
|
||||||
|
)?>" aria-label="<?=__('Password')?>" autocomplete="new-password" required autofocus>
|
||||||
|
</div>
|
||||||
|
<button type="submit" role="button" title="<?=__(
|
||||||
|
'Sign in'
|
||||||
|
)?>" class="btn btn-login btn-primary btn-block mt-2">
|
||||||
|
<?=__('Recover Account')?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -36,7 +36,7 @@ $assets->load('zxcvbn');
|
||||||
<i class="material-icons mr-1" aria-hidden="true">email</i>
|
<i class="material-icons mr-1" aria-hidden="true">email</i>
|
||||||
<strong><?=__('E-mail Address') ?></strong>
|
<strong><?=__('E-mail Address') ?></strong>
|
||||||
</label>
|
</label>
|
||||||
<input type="email" name="username" class="form-control" placeholder="" required>
|
<input type="email" id="username" name="username" class="form-control" placeholder="" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ $assets->load('zxcvbn');
|
||||||
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
||||||
<strong><?=__('Password') ?></strong>
|
<strong><?=__('Password') ?></strong>
|
||||||
</label>
|
</label>
|
||||||
<input type="password" name="password" class="form-control" placeholder="" required>
|
<input type="password" id="password" name="password" class="strength form-control" placeholder="" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
18
templates/mail/forgot.phtml
Normal file
18
templates/mail/forgot.phtml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
/** @var */
|
||||||
|
|
||||||
|
?><?=__('Account Recovery')?>
|
||||||
|
|
||||||
|
|
||||||
|
<?=__('An account recovery link has been requested for your account on "%s".', $environment->getAppName())?>
|
||||||
|
|
||||||
|
|
||||||
|
<?=__('Click the link below to log in to your account.')?>
|
||||||
|
|
||||||
|
|
||||||
|
<?=$router->named(
|
||||||
|
'account:recover',
|
||||||
|
['token' => $token],
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
)?>
|
|
@ -8,10 +8,18 @@ ini_set('display_errors', 1);
|
||||||
|
|
||||||
$autoloader = require dirname(__DIR__) . '/vendor/autoload.php';
|
$autoloader = require dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
$app = App\AppFactory::create($autoloader, [
|
$app = App\AppFactory::create(
|
||||||
|
$autoloader,
|
||||||
|
[
|
||||||
App\Environment::BASE_DIR => dirname(__DIR__),
|
App\Environment::BASE_DIR => dirname(__DIR__),
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$di = $app->getContainer();
|
$di = $app->getContainer();
|
||||||
|
|
||||||
App\Customization::initCli();
|
/** @var \Psr\Container\ContainerInterface|\DI\FactoryInterface $di */
|
||||||
|
$di = $app->getContainer();
|
||||||
|
|
||||||
|
/** @var \App\Locale $locale */
|
||||||
|
$locale = $di->make(\App\Locale::class);
|
||||||
|
$locale->register();
|
||||||
|
|
Loading…
Reference in a new issue