Implement SMTP Mail Delivery and Self-Service Password Reset (#3848)
This commit is contained in:
parent
4cd090decd
commit
c24f5dfc69
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace PHPSTORM_META {
|
||||
|
||||
override(
|
||||
\Psr\Container\ContainerInterface::get(0),
|
||||
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();
|
||||
|
||||
App\Customization::initCli();
|
||||
/** @var \App\Locale $locale */
|
||||
$locale = $di->make(\App\Locale::class);
|
||||
$locale->register();
|
||||
|
||||
/** @var App\Console\Application $cli */
|
||||
$cli = $di->get(App\Console\Application::class);
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
"symfony/event-dispatcher": "^5",
|
||||
"symfony/finder": "^5",
|
||||
"symfony/lock": "^5.1",
|
||||
"symfony/mailer": "^5.2",
|
||||
"symfony/messenger": "^5",
|
||||
"symfony/process": "^5",
|
||||
"symfony/property-access": "^5",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "8aed63d3424000ce9590c98912eb5a81",
|
||||
"content-hash": "8d2dbb48bbce314e087712c31d62a372",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
|
@ -102,21 +102,20 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AzuraCast/azuraforms.git",
|
||||
"reference": "70e51d4c1892392ad33529949f62cfb93ca5e48a"
|
||||
"reference": "8002d78f62a34cdb14df8967136547cdf6c04083"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/AzuraCast/azuraforms/zipball/70e51d4c1892392ad33529949f62cfb93ca5e48a",
|
||||
"reference": "70e51d4c1892392ad33529949f62cfb93ca5e48a",
|
||||
"url": "https://api.github.com/repos/AzuraCast/azuraforms/zipball/8002d78f62a34cdb14df8967136547cdf6c04083",
|
||||
"reference": "8002d78f62a34cdb14df8967136547cdf6c04083",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"overtrue/phplint": "^1.1",
|
||||
"phpstan/phpstan": "^0.11.1",
|
||||
"phpstan/phpstan-strict-rules": "^0.11.0",
|
||||
"overtrue/phplint": "^2.0",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"roave/security-advisories": "dev-master"
|
||||
},
|
||||
"default-branch": true,
|
||||
|
@ -150,7 +149,7 @@
|
|||
"issues": "https://github.com/AzuraCast/azuraforms/issues",
|
||||
"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",
|
||||
|
@ -1836,6 +1835,74 @@
|
|||
},
|
||||
"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",
|
||||
"version": "v1.0.3",
|
||||
|
@ -6937,6 +7004,87 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v5.2.3",
|
||||
|
@ -7025,6 +7173,88 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v1.22.1",
|
||||
|
@ -7104,6 +7334,93 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v1.22.1",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?php
|
||||
|
||||
use App\Customization;
|
||||
use App\Environment;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Locale;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
/**
|
||||
|
@ -132,7 +133,13 @@ return [
|
|||
'inline' => [
|
||||
'js' => [
|
||||
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];
|
||||
$localeShort = substr($locale, 0, 2);
|
||||
$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' => [
|
||||
'tab' => 'services',
|
||||
'use_grid' => true,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use App\Message;
|
||||
use App\Radio\Backend\Liquidsoap;
|
||||
use App\Sync\Task;
|
||||
use Symfony\Component\Mailer;
|
||||
|
||||
return [
|
||||
Message\AddNewMediaMessage::class => Task\CheckMediaTask::class,
|
||||
|
@ -17,4 +18,6 @@ return [
|
|||
Message\RunSyncTaskMessage::class => App\Sync\Runner::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')
|
||||
->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(
|
||||
'/setup',
|
||||
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\Supervisor::class => function (Environment $settings, Psr\Log\LoggerInterface $logger) {
|
||||
$client = new fXmlRpc\Client(
|
||||
|
|
|
@ -44,7 +44,7 @@ class Auth
|
|||
* @param string $username
|
||||
* @param string $password
|
||||
*/
|
||||
public function authenticate($username, $password): ?User
|
||||
public function authenticate(string $username, string $password): ?User
|
||||
{
|
||||
$user_auth = $this->userRepo->authenticate($username, $password);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
use App\Entity\Repository\SettingsRepository;
|
||||
use App\Entity\User;
|
||||
use App\Entity;
|
||||
use App\Exception\RateLimitExceededException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
|
@ -20,7 +19,8 @@ class LoginAction
|
|||
Response $response,
|
||||
EntityManagerInterface $em,
|
||||
RateLimit $rateLimit,
|
||||
SettingsRepository $settingsRepo
|
||||
Entity\Repository\SettingsRepository $settingsRepo,
|
||||
Entity\Repository\UserLoginTokenRepository $loginTokenRepo
|
||||
): ResponseInterface {
|
||||
$auth = $request->getAuth();
|
||||
$acl = $request->getAcl();
|
||||
|
@ -64,7 +64,7 @@ class LoginAction
|
|||
|
||||
$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.
|
||||
$session = $request->getSession();
|
||||
if ($session instanceof SessionCookiePersistenceInterface) {
|
||||
|
|
|
@ -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\Http\ServerRequest;
|
||||
use App\Service\NChan;
|
||||
use Gettext\Translator;
|
||||
use Locale;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class Customization
|
||||
{
|
||||
public const DEFAULT_LOCALE = 'en_US.UTF-8';
|
||||
public const DEFAULT_THEME = 'light';
|
||||
|
||||
public const THEME_LIGHT = 'light';
|
||||
|
@ -23,7 +20,7 @@ class Customization
|
|||
|
||||
protected Environment $environment;
|
||||
|
||||
protected string $locale = self::DEFAULT_LOCALE;
|
||||
protected Locale $locale;
|
||||
|
||||
protected string $theme = self::DEFAULT_THEME;
|
||||
|
||||
|
@ -44,8 +41,6 @@ class Customization
|
|||
// Register current user
|
||||
$this->user = $request->getAttribute(ServerRequest::ATTR_USER);
|
||||
|
||||
$this->locale = $this->initLocale($request);
|
||||
|
||||
// Register current theme
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
|
@ -59,88 +54,16 @@ class Customization
|
|||
}
|
||||
}
|
||||
|
||||
// Set up the PHP translator
|
||||
$translator = new Translator();
|
||||
|
||||
$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();
|
||||
|
||||
// Register translation superglobal functions
|
||||
setlocale(LC_ALL, $this->locale);
|
||||
// Register locale
|
||||
$this->locale = new Locale($environment, $request);
|
||||
$this->locale->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public function getLocale(): 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.
|
||||
*/
|
||||
|
@ -235,13 +158,4 @@ class Customization
|
|||
|
||||
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
|
||||
{
|
||||
use Traits\HasSplitTokenFields;
|
||||
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\JoinColumns({
|
||||
|
@ -53,13 +38,7 @@ class ApiKey implements JsonSerializable
|
|||
public function __construct(User $user, SplitToken $token)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->id = $token->identifier;
|
||||
$this->verifier = $token->hashVerifier();
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
$this->setFromToken($token);
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
|
@ -67,17 +46,6 @@ class ApiKey implements JsonSerializable
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
use App\Doctrine\Repository;
|
||||
use App\Entity;
|
||||
use App\Security\SplitToken;
|
||||
|
||||
class ApiKeyRepository extends Repository
|
||||
class ApiKeyRepository extends AbstractSplitTokenRepository
|
||||
{
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/**
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
*
|
||||
* @return bool|null|object
|
||||
*/
|
||||
public function authenticate($username, $password)
|
||||
public function find(int $id): ?Entity\User
|
||||
{
|
||||
$login_info = $this->repository->findOneBy(['email' => $username]);
|
||||
|
||||
if (!($login_info instanceof Entity\User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($login_info->verifyPassword($password)) {
|
||||
return $login_info;
|
||||
}
|
||||
return false;
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or returns an existing user with the specified e-mail address.
|
||||
*
|
||||
* @param string $email
|
||||
*/
|
||||
public function getOrCreate($email): Entity\User
|
||||
public function findByEmail(string $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)) {
|
||||
$user = new Entity\User();
|
||||
$user->setEmail($email);
|
||||
|
|
|
@ -784,4 +784,132 @@ class Settings
|
|||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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\Entity;
|
||||
use App\Exception;
|
||||
use App\Locale;
|
||||
use App\Radio;
|
||||
use App\RateLimit;
|
||||
use App\Session;
|
||||
|
@ -22,6 +23,7 @@ final class ServerRequest extends \Slim\Http\ServerRequest
|
|||
public const ATTR_ROUTER = 'app_router';
|
||||
public const ATTR_RATE_LIMIT = 'app_rate_limit';
|
||||
public const ATTR_ACL = 'acl';
|
||||
public const ATTR_LOCALE = 'locale';
|
||||
public const ATTR_CUSTOMIZATION = 'customization';
|
||||
public const ATTR_AUTH = 'auth';
|
||||
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);
|
||||
}
|
||||
|
||||
public function getLocale(): Locale
|
||||
{
|
||||
return $this->getAttributeOfClass(self::ATTR_LOCALE, Locale::class);
|
||||
}
|
||||
|
||||
public function getCustomization(): Customization
|
||||
{
|
||||
return $this->getAttributeOfClass(self::ATTR_CUSTOMIZATION, Customization::class);
|
||||
|
|
|
@ -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();
|
||||
|
||||
if (!$message instanceof AbstractMessage) {
|
||||
return [];
|
||||
return [
|
||||
$this->getTransport(self::QUEUE_NORMAL_PRIORITY),
|
||||
];
|
||||
}
|
||||
|
||||
$queue = $message->getQueue();
|
||||
|
|
|
@ -42,6 +42,8 @@ class GetCurrentUser implements MiddlewareInterface
|
|||
->withAttribute('is_logged_in', (null !== $user));
|
||||
|
||||
// Initialize Customization (timezones, locales, etc) based on the current logged in user.
|
||||
|
||||
/** @var Customization $customization */
|
||||
$customization = $this->factory->make(
|
||||
Customization::class,
|
||||
[
|
||||
|
@ -50,6 +52,8 @@ class GetCurrentUser implements MiddlewareInterface
|
|||
);
|
||||
|
||||
// Initialize ACL (can only be initialized after Customization as it contains localizations).
|
||||
|
||||
/** @var Acl $acl */
|
||||
$acl = $this->factory->make(
|
||||
Acl::class,
|
||||
[
|
||||
|
@ -58,7 +62,7 @@ class GetCurrentUser implements MiddlewareInterface
|
|||
);
|
||||
|
||||
$request = $request
|
||||
->withAttribute('locale', $customization->getLocale())
|
||||
->withAttribute(ServerRequest::ATTR_LOCALE, $customization->getLocale())
|
||||
->withAttribute(ServerRequest::ATTR_CUSTOMIZATION, $customization)
|
||||
->withAttribute(ServerRequest::ATTR_ACL, $acl);
|
||||
|
||||
|
|
|
@ -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 => [
|
||||
Task\RunAnalyticsTask::class,
|
||||
Task\RunAutomatedAssignmentTask::class,
|
||||
Task\CleanupLoginTokensTask::class,
|
||||
Task\CleanupHistoryTask::class,
|
||||
Task\CleanupStorageTask::class,
|
||||
Task\RotateLogsTask::class,
|
||||
|
|
45
src/View.php
45
src/View.php
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use App\Http\Router;
|
||||
use App\Http\ServerRequest;
|
||||
use DI\FactoryInterface;
|
||||
use Doctrine\Inflector\InflectorFactory;
|
||||
|
@ -16,24 +17,18 @@ class View extends Engine
|
|||
{
|
||||
protected Assets $assets;
|
||||
|
||||
protected ?ServerRequestInterface $request = null;
|
||||
|
||||
public function __construct(
|
||||
FactoryInterface $factory,
|
||||
Environment $environment,
|
||||
EventDispatcher $dispatcher,
|
||||
Version $version,
|
||||
ServerRequestInterface $request
|
||||
?ServerRequestInterface $request = null
|
||||
) {
|
||||
parent::__construct($environment->getViewsDirectory(), 'phtml');
|
||||
|
||||
// Add non-request-dependent content.
|
||||
$this->addData(
|
||||
[
|
||||
'environment' => $environment,
|
||||
'version' => $version,
|
||||
]
|
||||
);
|
||||
|
||||
// Add request-dependent content.
|
||||
$this->assets = $factory->make(
|
||||
Assets::class,
|
||||
[
|
||||
|
@ -43,17 +38,35 @@ class View extends Engine
|
|||
|
||||
$this->addData(
|
||||
[
|
||||
'request' => $request,
|
||||
'router' => $request->getAttribute(ServerRequest::ATTR_ROUTER),
|
||||
'auth' => $request->getAttribute(ServerRequest::ATTR_AUTH),
|
||||
'acl' => $request->getAttribute(ServerRequest::ATTR_ACL),
|
||||
'customization' => $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION),
|
||||
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
|
||||
'user' => $request->getAttribute(ServerRequest::ATTR_USER),
|
||||
'environment' => $environment,
|
||||
'version' => $version,
|
||||
'assets' => $this->assets,
|
||||
]
|
||||
);
|
||||
|
||||
// Add request-dependent content.
|
||||
$this->request = $request;
|
||||
|
||||
if ($request instanceof ServerRequestInterface) {
|
||||
$this->addData(
|
||||
[
|
||||
'request' => $request,
|
||||
'router' => $request->getAttribute(ServerRequest::ATTR_ROUTER),
|
||||
'auth' => $request->getAttribute(ServerRequest::ATTR_AUTH),
|
||||
'acl' => $request->getAttribute(ServerRequest::ATTR_ACL),
|
||||
'customization' => $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION),
|
||||
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
|
||||
'user' => $request->getAttribute(ServerRequest::ATTR_USER),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->addData(
|
||||
[
|
||||
'router' => $factory->make(Router::class),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->registerFunction(
|
||||
'escapeJs',
|
||||
function ($string) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
<strong><?=__('E-mail Address')?></strong>
|
||||
</label>
|
||||
<input type="email" name="username" class="form-control" placeholder="<?=__(
|
||||
<input type="email" id="username" name="username" class="form-control" placeholder="<?=__(
|
||||
'name@example.com'
|
||||
)?>" aria-label="<?=__('E-mail Address')?>" required autofocus>
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@ $this->layout(
|
|||
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
||||
<strong><?=__('Password')?></strong>
|
||||
</label>
|
||||
<input type="password" name="password" class="form-control" placeholder="<?=__(
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="<?=__(
|
||||
'Enter your password'
|
||||
)?>" aria-label="<?=__('Password')?>" required>
|
||||
</div>
|
||||
|
@ -69,8 +69,9 @@ $this->layout(
|
|||
</form>
|
||||
|
||||
<p class="text-center"><?=__('Please log in to continue.')?> <?=sprintf(
|
||||
__('<a href="%s" target="_blank">Forgot your password?</a>'),
|
||||
'https://docs.azuracast.com/en/administration/users#resetting-an-account-password'
|
||||
'<a href="%s">%s</a>',
|
||||
$router->named('account:forgot'),
|
||||
__('Forgot your password?')
|
||||
)?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
<strong><?=__('E-mail Address') ?></strong>
|
||||
</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>
|
||||
|
||||
|
@ -46,7 +46,7 @@ $assets->load('zxcvbn');
|
|||
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
||||
<strong><?=__('Password') ?></strong>
|
||||
</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>
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
$app = App\AppFactory::create($autoloader, [
|
||||
App\Environment::BASE_DIR => dirname(__DIR__),
|
||||
]);
|
||||
$app = App\AppFactory::create(
|
||||
$autoloader,
|
||||
[
|
||||
App\Environment::BASE_DIR => dirname(__DIR__),
|
||||
]
|
||||
);
|
||||
|
||||
$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 New Issue