Merged in feature/upgrade (pull request #33)
A bunch of changes and upgrades
This commit is contained in:
commit
e3436fe6db
17
.env
17
.env
|
@ -24,3 +24,20 @@ SENTRY_DSN=
|
||||||
# docker-compose
|
# docker-compose
|
||||||
PHP_FPM_PORT=9000
|
PHP_FPM_PORT=9000
|
||||||
APP_LOCAL_PATH=/var/www/magnetico-web/current
|
APP_LOCAL_PATH=/var/www/magnetico-web/current
|
||||||
|
|
||||||
|
###> symfony/mailer ###
|
||||||
|
MAILER_DSN=smtp://localhost
|
||||||
|
MAILER_FROM=no-reply@magnetico-web.tld
|
||||||
|
###< symfony/mailer ###
|
||||||
|
|
||||||
|
###> google/recaptcha ###
|
||||||
|
# To use Google Recaptcha, you must register a site on Recaptcha's admin panel:
|
||||||
|
# https://www.google.com/recaptcha/admin
|
||||||
|
#GOOGLE_RECAPTCHA_SITE_KEY=
|
||||||
|
#GOOGLE_RECAPTCHA_SECRET=
|
||||||
|
###< google/recaptcha ###
|
||||||
|
|
||||||
|
###> excelwebzone/recaptcha-bundle ###
|
||||||
|
EWZ_RECAPTCHA_SITE_KEY=
|
||||||
|
EWZ_RECAPTCHA_SECRET=
|
||||||
|
###< excelwebzone/recaptcha-bundle ###
|
||||||
|
|
|
@ -45,7 +45,7 @@ for production usage.
|
||||||
See [Symfony database configuration](https://symfony.com/doc/current/doctrine.html#configuring-the-database)
|
See [Symfony database configuration](https://symfony.com/doc/current/doctrine.html#configuring-the-database)
|
||||||
documentation for more details.
|
documentation for more details.
|
||||||
|
|
||||||
You **must** set environment variables for both databases: magneticod's SQLite and magnetico-web's PostgreSQL.
|
You **must** set environment variables for both databases: magneticod's and magnetico-web's PostgreSQL.
|
||||||
|
|
||||||
## Database schema migration
|
## Database schema migration
|
||||||
|
|
||||||
|
|
|
@ -12,17 +12,20 @@
|
||||||
],
|
],
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.2.0",
|
"php": "^7.4.0",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
|
"ext-hash": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
|
"excelwebzone/recaptcha-bundle": "^1.5",
|
||||||
"sensio/framework-extra-bundle": "^5.1",
|
"sensio/framework-extra-bundle": "^5.1",
|
||||||
"sentry/sentry-symfony": "^2.2",
|
|
||||||
"symfony/console": "^4.1",
|
"symfony/console": "^4.1",
|
||||||
"symfony/dotenv": "^4.1",
|
"symfony/dotenv": "^4.1",
|
||||||
"symfony/expression-language": "^4.1",
|
"symfony/expression-language": "^4.1",
|
||||||
"symfony/flex": "^1.0",
|
"symfony/flex": "^1.0",
|
||||||
"symfony/form": "^4.1",
|
"symfony/form": "^4.1",
|
||||||
"symfony/framework-bundle": "^4.1",
|
"symfony/framework-bundle": "^4.1",
|
||||||
|
"symfony/http-client": "^4.1",
|
||||||
|
"symfony/mailer": "^4.1",
|
||||||
"symfony/monolog-bundle": "^3.3",
|
"symfony/monolog-bundle": "^3.3",
|
||||||
"symfony/orm-pack": "^1.0",
|
"symfony/orm-pack": "^1.0",
|
||||||
"symfony/security-bundle": "^4.1",
|
"symfony/security-bundle": "^4.1",
|
||||||
|
|
4223
composer.lock
generated
4223
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true],
|
|
||||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||||
|
@ -13,4 +12,5 @@ return [
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
Sentry\SentryBundle\SentryBundle::class => ['all' => true],
|
Sentry\SentryBundle\SentryBundle::class => ['all' => true],
|
||||||
|
EWZ\Bundle\RecaptchaBundle\EWZRecaptchaBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|
2
config/packages/dev/ewz_recaptcha.yaml
Normal file
2
config/packages/dev/ewz_recaptcha.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ewz_recaptcha:
|
||||||
|
enabled: false
|
4
config/packages/ewz_recaptcha.yaml
Normal file
4
config/packages/ewz_recaptcha.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# See https://github.com/excelwebzone/EWZRecaptchaBundle for full configuration
|
||||||
|
ewz_recaptcha:
|
||||||
|
public_key: '%env(EWZ_RECAPTCHA_SITE_KEY)%'
|
||||||
|
private_key: '%env(EWZ_RECAPTCHA_SECRET)%'
|
21
config/packages/google_recaptcha.yaml
Normal file
21
config/packages/google_recaptcha.yaml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#services:
|
||||||
|
#
|
||||||
|
# # Inject this service in your controllers/services to verify a submitted captcha.
|
||||||
|
# ReCaptcha\ReCaptcha:
|
||||||
|
# arguments:
|
||||||
|
# $secret: '%env(GOOGLE_RECAPTCHA_SECRET)%'
|
||||||
|
# $requestMethod: '@ReCaptcha\RequestMethod'
|
||||||
|
#
|
||||||
|
# # Curl is set here as default transport to communicate with Google servers.
|
||||||
|
# # If you do not have php-curl extension, you can change for a socket or a plain POST request.
|
||||||
|
# # Check out the repository for all other request methods:
|
||||||
|
# # https://github.com/google/recaptcha/tree/master/src/ReCaptcha/RequestMethod
|
||||||
|
# ReCaptcha\RequestMethod: '@ReCaptcha\RequestMethod\CurlPost'
|
||||||
|
# ReCaptcha\RequestMethod\CurlPost: null
|
||||||
|
# ReCaptcha\RequestMethod\Curl: null
|
||||||
|
#
|
||||||
|
## Uncomment this line if you want to inject the site key to all your Twig templates.
|
||||||
|
## You can also inject the "google_recaptcha_site_key" container parameter to your controllers.
|
||||||
|
##twig:
|
||||||
|
## globals:
|
||||||
|
## google_recaptcha_site_key: '%google_recaptcha_site_key%'
|
3
config/packages/mailer.yaml
Normal file
3
config/packages/mailer.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
framework:
|
||||||
|
mailer:
|
||||||
|
dsn: '%env(MAILER_DSN)%'
|
|
@ -15,3 +15,6 @@ monolog:
|
||||||
type: console
|
type: console
|
||||||
process_psr_3_messages: false
|
process_psr_3_messages: false
|
||||||
channels: ["!event", "!doctrine"]
|
channels: ["!event", "!doctrine"]
|
||||||
|
sentry:
|
||||||
|
type: service
|
||||||
|
id: Sentry\Monolog\Handler
|
||||||
|
|
|
@ -8,10 +8,8 @@ security:
|
||||||
manager_name: default
|
manager_name: default
|
||||||
encoders:
|
encoders:
|
||||||
App\Entity\User:
|
App\Entity\User:
|
||||||
algorithm: 'argon2i'
|
# https://symfony.com/blog/new-in-symfony-4-3-native-password-encoder
|
||||||
memory_cost: 16384
|
algorithm: 'auto'
|
||||||
time_cost: 2
|
|
||||||
threads: 4
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
|
@ -28,10 +26,10 @@ security:
|
||||||
anonymous: ~
|
anonymous: ~
|
||||||
provider: default_provider
|
provider: default_provider
|
||||||
form_login:
|
form_login:
|
||||||
login_path: user_login
|
login_path: user_auth_login
|
||||||
check_path: user_login
|
check_path: user_auth_login
|
||||||
logout:
|
logout:
|
||||||
path: user_logout
|
path: user_auth_logout
|
||||||
target: /
|
target: /
|
||||||
remember_me:
|
remember_me:
|
||||||
secret: '%kernel.secret%'
|
secret: '%kernel.secret%'
|
||||||
|
@ -46,7 +44,7 @@ security:
|
||||||
- { path: ^/api/v1/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: ^/api/v1/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: ^/api/, roles: ROLE_USER }
|
- { path: ^/api/, roles: ROLE_USER }
|
||||||
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: ^/auth/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: ^/register/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: ^/register/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: ^/magnet/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: ^/magnet/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: ^/, roles: ROLE_USER }
|
- { path: ^/, roles: ROLE_USER }
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
sentry:
|
sentry:
|
||||||
options:
|
options:
|
||||||
curl_method: async
|
send_default_pii: true
|
||||||
|
excluded_exceptions:
|
||||||
# skip_capture: # To skip certain exceptions, specify a list below
|
- 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'
|
||||||
# - 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'
|
- 'Symfony\Component\HttpKernel\Exception\BadRequestHttpException'
|
||||||
# - 'Symfony\Component\HttpKernel\Exception\BadRequestHttpException'
|
- 'Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException'
|
||||||
# - 'Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException'
|
monolog:
|
||||||
|
error_handler:
|
||||||
|
enabled: true
|
||||||
|
level: error
|
||||||
|
|
|
@ -26,25 +26,33 @@ magnet_redirect:
|
||||||
infoHash: '[0-9a-fA-F]{40}'
|
infoHash: '[0-9a-fA-F]{40}'
|
||||||
|
|
||||||
user_register:
|
user_register:
|
||||||
path: /register/{inviteCode}
|
path: /register/{code}
|
||||||
controller: App\Controller\UserController::register
|
controller: App\Controller\UserController::register
|
||||||
requirements:
|
requirements:
|
||||||
method: GET
|
method: GET
|
||||||
inviteCode: \w{32}
|
inviteCode: \w{32}
|
||||||
|
|
||||||
|
user_reset_request:
|
||||||
|
path: /auth/reset/request
|
||||||
|
controller: App\Controller\UserController::requestReset
|
||||||
|
|
||||||
|
user_reset:
|
||||||
|
path: /auth/reset/{code}
|
||||||
|
controller: App\Controller\UserController::reset
|
||||||
|
|
||||||
|
user_auth_login:
|
||||||
|
path: /auth/login
|
||||||
|
controller: App\Controller\SecurityController::login
|
||||||
|
|
||||||
|
user_auth_logout:
|
||||||
|
path: /auth/logout
|
||||||
|
|
||||||
user_account_invites:
|
user_account_invites:
|
||||||
path: /account/invites
|
path: /account/invites
|
||||||
controller: App\Controller\AccountController::invites
|
controller: App\Controller\AccountController::invites
|
||||||
requirements:
|
requirements:
|
||||||
method: GET
|
method: GET
|
||||||
|
|
||||||
user_login:
|
|
||||||
path: /login
|
|
||||||
controller: App\Controller\SecurityController::login
|
|
||||||
|
|
||||||
user_logout:
|
|
||||||
path: /logout
|
|
||||||
|
|
||||||
# API
|
# API
|
||||||
api_v1_login:
|
api_v1_login:
|
||||||
path: /api/v1/login
|
path: /api/v1/login
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
parameters:
|
parameters:
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
env(TRACKER_LIST_FILE): '%kernel.project_dir%/config/public_trackers.json'
|
env(TRACKER_LIST_FILE): '%kernel.project_dir%/config/public_trackers.json'
|
||||||
env(NEW_USER_INVITES): 10
|
env(NEW_USER_INVITES): '10'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
|
@ -16,6 +16,7 @@ services:
|
||||||
bind:
|
bind:
|
||||||
$publicTrackers: '%env(json:file:resolve:TRACKER_LIST_FILE)%'
|
$publicTrackers: '%env(json:file:resolve:TRACKER_LIST_FILE)%'
|
||||||
$newUserInvites: '%env(NEW_USER_INVITES)%'
|
$newUserInvites: '%env(NEW_USER_INVITES)%'
|
||||||
|
$fromAddress: '%env(MAILER_FROM)%'
|
||||||
|
|
||||||
App\:
|
App\:
|
||||||
resource: '../src/*'
|
resource: '../src/*'
|
||||||
|
@ -44,3 +45,6 @@ services:
|
||||||
App\Search\TorrentSearcher:
|
App\Search\TorrentSearcher:
|
||||||
arguments:
|
arguments:
|
||||||
$classMetadata: '@=service(''doctrine.orm.magneticod_entity_manager'').getClassMetadata(''App\\Magnetico\\Entity\\Torrent'')'
|
$classMetadata: '@=service(''doctrine.orm.magneticod_entity_manager'').getClassMetadata(''App\\Magnetico\\Entity\\Torrent'')'
|
||||||
|
|
||||||
|
Monolog\Processor\PsrLogMessageProcessor:
|
||||||
|
tags: { name: monolog.processor, handler: sentry }
|
||||||
|
|
|
@ -20,7 +20,7 @@ class MainController extends AbstractController
|
||||||
private function createLoginForm(string $username): FormInterface
|
private function createLoginForm(string $username): FormInterface
|
||||||
{
|
{
|
||||||
$form = $this->createForm(LoginType::class, null, [
|
$form = $this->createForm(LoginType::class, null, [
|
||||||
'action' => $this->generateUrl('user_login'),
|
'action' => $this->generateUrl('user_auth_login'),
|
||||||
]);
|
]);
|
||||||
$form->get('_username')->setData($username);
|
$form->get('_username')->setData($username);
|
||||||
$form->add('submit', SubmitType::class);
|
$form->add('submit', SubmitType::class);
|
||||||
|
|
|
@ -29,7 +29,7 @@ class SecurityController extends AbstractController
|
||||||
private function createLoginForm(string $username): FormInterface
|
private function createLoginForm(string $username): FormInterface
|
||||||
{
|
{
|
||||||
$form = $this->createForm(LoginType::class, null, [
|
$form = $this->createForm(LoginType::class, null, [
|
||||||
'action' => $this->generateUrl('user_login'),
|
'action' => $this->generateUrl('user_auth_login'),
|
||||||
]);
|
]);
|
||||||
$form->get('_username')->setData($username);
|
$form->get('_username')->setData($username);
|
||||||
$form->add('submit', SubmitType::class);
|
$form->add('submit', SubmitType::class);
|
||||||
|
|
|
@ -2,38 +2,39 @@
|
||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Form\{CreateUserRequestType};
|
use App\Entity\{Invite, PasswordResetToken};
|
||||||
use App\FormRequest\CreateUserRequest;
|
use App\Repository\PasswordResetTokenRepository;
|
||||||
|
use App\Form\{Data\PasswordResetRequestData, Data\PasswordResetData, PasswordResetRequestType, PasswordResetType, RegisterType, Data\RegisterData};
|
||||||
use App\Repository\InviteRepository;
|
use App\Repository\InviteRepository;
|
||||||
use App\User\{InviteManager, UserManager};
|
use App\User\{Exception\UserNotFoundException, InviteManager, PasswordResetManager, UserManager};
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\{Extension\Core\Type\SubmitType, FormError, FormInterface};
|
||||||
use Symfony\Component\Form\FormInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\{Request, Response};
|
use Symfony\Component\HttpFoundation\{Request, Response};
|
||||||
|
|
||||||
class UserController extends AbstractController
|
class UserController extends AbstractController
|
||||||
{
|
{
|
||||||
public function register(
|
public function register(
|
||||||
string $inviteCode,
|
string $code,
|
||||||
Request $request,
|
Request $request,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
UserManager $userManager,
|
UserManager $userManager,
|
||||||
InviteManager $inviteManager,
|
InviteManager $inviteManager,
|
||||||
InviteRepository $inviteRepo
|
InviteRepository $inviteRepo
|
||||||
): Response {
|
): Response {
|
||||||
$createUserRequest = new CreateUserRequest($inviteCode);
|
$formData = new RegisterData($code);
|
||||||
$form = $this->createRegisterForm($createUserRequest, $inviteCode);
|
$form = $this->createRegisterForm($formData, $code);
|
||||||
|
|
||||||
$invite = $inviteRepo->findOneBy(['code' => $inviteCode, 'usedBy' => null]);
|
/** @var Invite $invite */
|
||||||
|
$invite = $inviteRepo->findOneBy(['code' => $code, 'usedBy' => null]);
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$user = $userManager->createUserByInvite(
|
$user = $userManager->createUserByInvite(
|
||||||
$createUserRequest->username,
|
$formData->username,
|
||||||
$createUserRequest->password,
|
$formData->password,
|
||||||
$createUserRequest->email,
|
$formData->email,
|
||||||
$invite
|
$invite
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -46,14 +47,96 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
return $this->render('User/register.html.twig', [
|
return $this->render('User/register.html.twig', [
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
'inviteValid' => $invite ? true : null,
|
'invite' => $invite,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createRegisterForm(CreateUserRequest $createUserRequest, string $inviteCode): FormInterface
|
public function requestReset(Request $request, EntityManagerInterface $em, PasswordResetManager $manager): Response
|
||||||
{
|
{
|
||||||
$form = $this->createForm(CreateUserRequestType::class, $createUserRequest, [
|
$formData = new PasswordResetRequestData();
|
||||||
'action' => $this->generateUrl('user_register', ['inviteCode' => $inviteCode]),
|
$form = $this->createResetRequestForm($formData);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
$message = null;
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
try {
|
||||||
|
$manager->sendResetLink($formData->email);
|
||||||
|
|
||||||
|
$message = 'Password reset link was sent';
|
||||||
|
} catch (UserNotFoundException $e) {
|
||||||
|
$form->addError(new FormError('User not found'));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Sentry\captureException($e);
|
||||||
|
$form->addError(new FormError('Something happened. Try again later or contact the administrator.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('User/reset.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(
|
||||||
|
string $code,
|
||||||
|
Request $request,
|
||||||
|
EntityManagerInterface $em,
|
||||||
|
UserManager $manager,
|
||||||
|
PasswordResetTokenRepository $tokenRepository
|
||||||
|
): Response
|
||||||
|
{
|
||||||
|
$formData = new PasswordResetData();
|
||||||
|
$form = $this->createResetForm($formData, $code);
|
||||||
|
|
||||||
|
/** @var PasswordResetToken $token */
|
||||||
|
$token = $tokenRepository->find($code);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if ($token && $token->isValid()) {
|
||||||
|
$manager->changePassword($token->getUser(), $formData->password);
|
||||||
|
|
||||||
|
$em->remove($token);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('user_auth_login');
|
||||||
|
} else {
|
||||||
|
$form->addError(new FormError('Invalid token.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('User/reset.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createResetRequestForm(PasswordResetRequestData $formData): FormInterface
|
||||||
|
{
|
||||||
|
$form = $this->createForm(PasswordResetRequestType::class, $formData, [
|
||||||
|
'action' => $this->generateUrl('user_reset_request'),
|
||||||
|
]);
|
||||||
|
$form->add('submit', SubmitType::class);
|
||||||
|
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createResetForm(PasswordResetData $formData, string $code): FormInterface
|
||||||
|
{
|
||||||
|
$form = $this->createForm(PasswordResetType::class, $formData, [
|
||||||
|
'action' => $this->generateUrl('user_reset', ['code' => $code]),
|
||||||
|
]);
|
||||||
|
$form->add('submit', SubmitType::class);
|
||||||
|
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createRegisterForm(RegisterData $formData, string $code): FormInterface
|
||||||
|
{
|
||||||
|
$form = $this->createForm(RegisterType::class, $formData, [
|
||||||
|
'action' => $this->generateUrl('user_register', ['code' => $code]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$form->add('submit', SubmitType::class);
|
$form->add('submit', SubmitType::class);
|
||||||
|
|
67
src/Entity/PasswordResetToken.php
Normal file
67
src/Entity/PasswordResetToken.php
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Table(schema="users", name="password_reset_tokens")
|
||||||
|
* @ORM\Entity(readOnly=true)
|
||||||
|
*/
|
||||||
|
class PasswordResetToken
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*
|
||||||
|
* @ORM\ManyToOne(targetEntity="User", fetch="EAGER")
|
||||||
|
* @ORM\JoinColumn(name="user_id", nullable=false, onDelete="CASCADE")
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ORM\Id()
|
||||||
|
* @ORM\Column(name="code", type="text", nullable=false)
|
||||||
|
*/
|
||||||
|
private $code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \DateTime
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="valid_until", type="datetime", nullable=false)
|
||||||
|
*/
|
||||||
|
private $validUntil;
|
||||||
|
|
||||||
|
public function __construct(User $user, \DateInterval $validFor = null)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
$this->code = hash('sha3-384', uniqid('reset', true));
|
||||||
|
|
||||||
|
$now = new \DateTime();
|
||||||
|
|
||||||
|
if ($validFor) {
|
||||||
|
$this->validUntil = $now->add($validFor);
|
||||||
|
} else {
|
||||||
|
$this->validUntil = $now->add(new \DateInterval('P1D'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
$now = new \DateTime();
|
||||||
|
|
||||||
|
return $now < $this->validUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCode(): string
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ namespace App\Entity;
|
||||||
|
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,10 +64,10 @@ class User implements UserInterface, \Serializable
|
||||||
*/
|
*/
|
||||||
private $invites;
|
private $invites;
|
||||||
|
|
||||||
public function __construct(string $username, string $password, string $email, array $roles = [])
|
public function __construct(string $username, PasswordEncoderInterface $encoder, string $rawPassword, string $email, array $roles = [])
|
||||||
{
|
{
|
||||||
$this->username = $username;
|
$this->username = $username;
|
||||||
$this->password = $password;
|
$this->password = $encoder->encodePassword($rawPassword, null);
|
||||||
$this->email = $email;
|
$this->email = $email;
|
||||||
$this->roles = $roles ?: ['ROLE_USER'];
|
$this->roles = $roles ?: ['ROLE_USER'];
|
||||||
$this->createdAt = new \DateTime();
|
$this->createdAt = new \DateTime();
|
||||||
|
@ -87,9 +88,9 @@ class User implements UserInterface, \Serializable
|
||||||
return $this->password;
|
return $this->password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatePassword(string $password): void
|
public function changePassword(PasswordEncoderInterface $encoder, string $rawPassword): void
|
||||||
{
|
{
|
||||||
$this->password = $password;
|
$this->password = $encoder->encodePassword($rawPassword, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSalt()
|
public function getSalt()
|
||||||
|
@ -143,10 +144,10 @@ class User implements UserInterface, \Serializable
|
||||||
/** @see \Serializable::unserialize() */
|
/** @see \Serializable::unserialize() */
|
||||||
public function unserialize($serialized)
|
public function unserialize($serialized)
|
||||||
{
|
{
|
||||||
list(
|
[
|
||||||
$this->id,
|
$this->id,
|
||||||
$this->username,
|
$this->username,
|
||||||
$this->password
|
$this->password
|
||||||
) = unserialize($serialized, ['allowed_classes' => false]);
|
] = unserialize($serialized, ['allowed_classes' => false]);
|
||||||
}
|
}
|
||||||
}
|
}
|
17
src/Form/Data/PasswordResetData.php
Normal file
17
src/Form/Data/PasswordResetData.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Data;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
class PasswordResetData
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @Assert\NotBlank
|
||||||
|
* @Assert\Length(min="8", max="4096")
|
||||||
|
* @Assert\NotCompromisedPassword(skipOnError=true)
|
||||||
|
*/
|
||||||
|
public $password;
|
||||||
|
}
|
24
src/Form/Data/PasswordResetRequestData.php
Normal file
24
src/Form/Data/PasswordResetRequestData.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Data;
|
||||||
|
|
||||||
|
use EWZ\Bundle\RecaptchaBundle\Validator\Constraints as ReCaptcha;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
class PasswordResetRequestData
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @Assert\Email()
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
*/
|
||||||
|
public $email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ReCaptcha\IsTrue
|
||||||
|
*/
|
||||||
|
public $recaptcha;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\FormRequest;
|
namespace App\Form\Data;
|
||||||
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use App\Validator\Constraints as AppAssert;
|
use App\Validator\Constraints as AppAssert;
|
||||||
|
@ -8,7 +8,7 @@ use App\Validator\Constraints as AppAssert;
|
||||||
/**
|
/**
|
||||||
* @todo implement UniqueEntity constraint for DTO and use it here
|
* @todo implement UniqueEntity constraint for DTO and use it here
|
||||||
*/
|
*/
|
||||||
class CreateUserRequest
|
class RegisterData
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
|
@ -11,8 +11,8 @@ class LoginType extends AbstractType
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('_username', TextType::class, ['mapped' => false])
|
->add('_username', TextType::class, ['mapped' => false, 'required' => true])
|
||||||
->add('_password', PasswordType::class, ['mapped' => false])
|
->add('_password', PasswordType::class, ['mapped' => false, 'required' => true])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,4 @@ class LoginType extends AbstractType
|
||||||
// Empty prefix for default UsernamePasswordFrormAuthenticationListener
|
// Empty prefix for default UsernamePasswordFrormAuthenticationListener
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
26
src/Form/PasswordResetRequestType.php
Normal file
26
src/Form/PasswordResetRequestType.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Form\Data\PasswordResetRequestData;
|
||||||
|
use Symfony\Component\Form\{AbstractType, Extension\Core\Type\EmailType, FormBuilderInterface};
|
||||||
|
use EWZ\Bundle\RecaptchaBundle\Form\Type\EWZRecaptchaType;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class PasswordResetRequestType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('email', EmailType::class, ['required' => true])
|
||||||
|
->add('recaptcha', EWZRecaptchaType::class)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => PasswordResetRequestData::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
32
src/Form/PasswordResetType.php
Normal file
32
src/Form/PasswordResetType.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Form\Data\PasswordResetData;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\{HiddenType, PasswordType, RepeatedType};
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class PasswordResetType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('password', RepeatedType::class, [
|
||||||
|
'type' => PasswordType::class,
|
||||||
|
'invalid_message' => 'The password fields must match.',
|
||||||
|
'required' => true,
|
||||||
|
'first_options' => ['label' => 'Password'],
|
||||||
|
'second_options' => ['label' => 'Repeat'],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => PasswordResetData::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,29 +2,28 @@
|
||||||
|
|
||||||
namespace App\Form;
|
namespace App\Form;
|
||||||
|
|
||||||
use App\FormRequest\CreateUserRequest;
|
use App\Form\Data\RegisterData;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\{EmailType, PasswordType, TextType};
|
use Symfony\Component\Form\Extension\Core\Type\{EmailType, PasswordType, TextType};
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class CreateUserRequestType extends AbstractType
|
class RegisterType extends AbstractType
|
||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('username', TextType::class)
|
->add('username', TextType::class, ['required' => true])
|
||||||
->add('password', PasswordType::class)
|
->add('password', PasswordType::class, ['required' => true])
|
||||||
->add('email', EmailType::class)
|
->add('email', EmailType::class, ['required' => true])
|
||||||
->add('inviteCode', TextType::class)
|
->add('inviteCode', TextType::class, ['required' => true])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => CreateUserRequest::class,
|
'data_class' => RegisterData::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,11 +4,11 @@ namespace App\Magnetico\Repository;
|
||||||
|
|
||||||
use App\Magnetico\Entity\Torrent;
|
use App\Magnetico\Entity\Torrent;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Symfony\Bridge\Doctrine\RegistryInterface;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
class TorrentRepository extends ServiceEntityRepository
|
class TorrentRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
public function __construct(RegistryInterface $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
{
|
{
|
||||||
parent::__construct($registry, Torrent::class);
|
parent::__construct($registry, Torrent::class);
|
||||||
}
|
}
|
||||||
|
|
34
src/Migrations/Version20200118004840.php
Normal file
34
src/Migrations/Version20200118004840.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20200118004840 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription() : string
|
||||||
|
{
|
||||||
|
return 'Adding password reset tokens.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema) : void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||||
|
|
||||||
|
$this->addSql('CREATE TABLE users.password_reset_tokens (code TEXT NOT NULL, user_id INT NOT NULL, valid_until TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(code))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_261A8E65A76ED395 ON users.password_reset_tokens (user_id)');
|
||||||
|
$this->addSql('ALTER TABLE users.password_reset_tokens ADD CONSTRAINT FK_CCD4B965A76ED395 FOREIGN KEY (user_id) REFERENCES users.users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema) : void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||||
|
|
||||||
|
$this->addSql('DROP TABLE users.password_reset_tokens');
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,11 @@ namespace App\Repository;
|
||||||
use App\Entity\{ApiToken, User};
|
use App\Entity\{ApiToken, User};
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Symfony\Bridge\Doctrine\RegistryInterface;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
class ApiTokenRepository extends ServiceEntityRepository
|
class ApiTokenRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
public function __construct(RegistryInterface $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
{
|
{
|
||||||
parent::__construct($registry, ApiToken::class);
|
parent::__construct($registry, ApiToken::class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use App\Entity\{Invite, User};
|
use App\Entity\{Invite, User};
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Symfony\Bridge\Doctrine\RegistryInterface;
|
|
||||||
|
|
||||||
class InviteRepository extends ServiceEntityRepository
|
class InviteRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
public function __construct(RegistryInterface $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
{
|
{
|
||||||
parent::__construct($registry, Invite::class);
|
parent::__construct($registry, Invite::class);
|
||||||
}
|
}
|
||||||
|
|
20
src/Repository/PasswordResetTokenRepository.php
Normal file
20
src/Repository/PasswordResetTokenRepository.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\PasswordResetToken;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class PasswordResetTokenRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, PasswordResetToken::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(PasswordResetToken $token): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->persist($token);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,11 @@ namespace App\Repository;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Symfony\Bridge\Doctrine\RegistryInterface;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
class UserRepository extends ServiceEntityRepository
|
class UserRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
public function __construct(RegistryInterface $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
{
|
{
|
||||||
parent::__construct($registry, User::class);
|
parent::__construct($registry, User::class);
|
||||||
}
|
}
|
||||||
|
|
8
src/User/Exception/UserNotFoundException.php
Normal file
8
src/User/Exception/UserNotFoundException.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Exception;
|
||||||
|
|
||||||
|
class UserNotFoundException extends \InvalidArgumentException
|
||||||
|
{
|
||||||
|
protected $message = 'User not found';
|
||||||
|
}
|
79
src/User/PasswordResetManager.php
Normal file
79
src/User/PasswordResetManager.php
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\User;
|
||||||
|
|
||||||
|
use App\Repository\PasswordResetTokenRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use App\Entity\{PasswordResetToken, User};
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use App\User\Exception\UserNotFoundException;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Routing\{Generator\UrlGeneratorInterface, RouterInterface};
|
||||||
|
|
||||||
|
class PasswordResetManager
|
||||||
|
{
|
||||||
|
/** @var UserRepository */
|
||||||
|
private $userRepo;
|
||||||
|
|
||||||
|
/** @var PasswordResetTokenRepository */
|
||||||
|
private $tokenRepo;
|
||||||
|
|
||||||
|
/** @var EntityManagerInterface */
|
||||||
|
private $em;
|
||||||
|
|
||||||
|
/** @var MailerInterface */
|
||||||
|
private $mailer;
|
||||||
|
|
||||||
|
/** @var RouterInterface */
|
||||||
|
private $router;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private $fromAddress;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
UserRepository $userRepo,
|
||||||
|
PasswordResetTokenRepository $tokenRepo,
|
||||||
|
EntityManagerInterface $em,
|
||||||
|
MailerInterface $mailer,
|
||||||
|
RouterInterface $router,
|
||||||
|
string $fromAddress
|
||||||
|
) {
|
||||||
|
$this->userRepo = $userRepo;
|
||||||
|
$this->tokenRepo = $tokenRepo;
|
||||||
|
$this->em = $em;
|
||||||
|
$this->mailer = $mailer;
|
||||||
|
$this->router = $router;
|
||||||
|
$this->fromAddress = $fromAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendResetLink(string $address): void
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
if (null === $user = $this->userRepo->findOneBy(['email' => $address])) {
|
||||||
|
throw new UserNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo add limits
|
||||||
|
$token = new PasswordResetToken($user);
|
||||||
|
$this->tokenRepo->add($token);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$link = $this->router->generate('user_reset', ['code' => $token->getCode()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||||
|
|
||||||
|
$mail = (new Email())
|
||||||
|
->from($this->fromAddress)
|
||||||
|
->to($user->getEmail())
|
||||||
|
->subject('Password reset')
|
||||||
|
->text(<<<MAIL
|
||||||
|
Here is your password reset link:
|
||||||
|
|
||||||
|
$link
|
||||||
|
MAIL
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->mailer->send($mail);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,11 +29,10 @@ class UserManager
|
||||||
|
|
||||||
public function createUser(string $username, string $password, string $email, array $roles = self::DEFAULT_ROLES): User
|
public function createUser(string $username, string $password, string $email, array $roles = self::DEFAULT_ROLES): User
|
||||||
{
|
{
|
||||||
$encodedPassword = $this->encoderFactory->getEncoder(User::class)->encodePassword($password, null);
|
|
||||||
|
|
||||||
$user = new User(
|
$user = new User(
|
||||||
$username,
|
$username,
|
||||||
$encodedPassword,
|
$this->encoderFactory->getEncoder(User::class),
|
||||||
|
$password,
|
||||||
$email,
|
$email,
|
||||||
$roles
|
$roles
|
||||||
);
|
);
|
||||||
|
@ -43,6 +42,14 @@ class UserManager
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function changePassword(User $user, string $rawPassword): void
|
||||||
|
{
|
||||||
|
$user->changePassword(
|
||||||
|
$this->encoderFactory->getEncoder(User::class),
|
||||||
|
$rawPassword
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function createUserByInvite(string $username, string $password, string $email, Invite $invite, array $roles = self::DEFAULT_ROLES): User
|
public function createUserByInvite(string $username, string $password, string $email, Invite $invite, array $roles = self::DEFAULT_ROLES): User
|
||||||
{
|
{
|
||||||
if (null !== $invite->getUsedBy()) {
|
if (null !== $invite->getUsedBy()) {
|
||||||
|
|
118
symfony.lock
118
symfony.lock
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"clue/stream-filter": {
|
||||||
|
"version": "v1.4.1"
|
||||||
|
},
|
||||||
"doctrine/annotations": {
|
"doctrine/annotations": {
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
@ -29,9 +32,6 @@
|
||||||
"ref": "ae205d5114e719deb64d2110f56ef910787d1e04"
|
"ref": "ae205d5114e719deb64d2110f56ef910787d1e04"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"doctrine/doctrine-cache-bundle": {
|
|
||||||
"version": "1.3.3"
|
|
||||||
},
|
|
||||||
"doctrine/doctrine-migrations-bundle": {
|
"doctrine/doctrine-migrations-bundle": {
|
||||||
"version": "1.2",
|
"version": "1.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
@ -65,12 +65,55 @@
|
||||||
"doctrine/reflection": {
|
"doctrine/reflection": {
|
||||||
"version": "v1.0.0"
|
"version": "v1.0.0"
|
||||||
},
|
},
|
||||||
|
"egulias/email-validator": {
|
||||||
|
"version": "2.1.14"
|
||||||
|
},
|
||||||
|
"excelwebzone/recaptcha-bundle": {
|
||||||
|
"version": "1.5",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes-contrib",
|
||||||
|
"branch": "master",
|
||||||
|
"version": "1.5",
|
||||||
|
"ref": "fd4da7bc71749db65bc83abf5d164bfa9c839cf4"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/dev/ewz_recaptcha.yaml",
|
||||||
|
"config/packages/ewz_recaptcha.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"google/recaptcha": {
|
||||||
|
"version": "1.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes-contrib",
|
||||||
|
"branch": "master",
|
||||||
|
"version": "1.1",
|
||||||
|
"ref": "d087df3e087f50da3955f2def05079380da5894b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/google_recaptcha.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"guzzlehttp/guzzle": {
|
||||||
|
"version": "6.5.2"
|
||||||
|
},
|
||||||
|
"guzzlehttp/promises": {
|
||||||
|
"version": "v1.3.1"
|
||||||
|
},
|
||||||
|
"guzzlehttp/psr7": {
|
||||||
|
"version": "1.6.1"
|
||||||
|
},
|
||||||
|
"http-interop/http-factory-guzzle": {
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
"jdorn/sql-formatter": {
|
"jdorn/sql-formatter": {
|
||||||
"version": "v1.2.17"
|
"version": "v1.2.17"
|
||||||
},
|
},
|
||||||
"jean85/pretty-package-versions": {
|
"jean85/pretty-package-versions": {
|
||||||
"version": "1.2"
|
"version": "1.2"
|
||||||
},
|
},
|
||||||
|
"laminas/laminas-zendframework-bridge": {
|
||||||
|
"version": "1.0.1"
|
||||||
|
},
|
||||||
"monolog/monolog": {
|
"monolog/monolog": {
|
||||||
"version": "1.23.0"
|
"version": "1.23.0"
|
||||||
},
|
},
|
||||||
|
@ -83,6 +126,33 @@
|
||||||
"pagerfanta/pagerfanta": {
|
"pagerfanta/pagerfanta": {
|
||||||
"version": "v2.0.1"
|
"version": "v2.0.1"
|
||||||
},
|
},
|
||||||
|
"paragonie/random_compat": {
|
||||||
|
"version": "v9.99.99"
|
||||||
|
},
|
||||||
|
"php": {
|
||||||
|
"version": "7.4"
|
||||||
|
},
|
||||||
|
"php-http/client-common": {
|
||||||
|
"version": "2.1.0"
|
||||||
|
},
|
||||||
|
"php-http/discovery": {
|
||||||
|
"version": "1.7.4"
|
||||||
|
},
|
||||||
|
"php-http/guzzle6-adapter": {
|
||||||
|
"version": "v2.0.1"
|
||||||
|
},
|
||||||
|
"php-http/httplug": {
|
||||||
|
"version": "2.1.0"
|
||||||
|
},
|
||||||
|
"php-http/message": {
|
||||||
|
"version": "1.8.0"
|
||||||
|
},
|
||||||
|
"php-http/message-factory": {
|
||||||
|
"version": "v1.0.2"
|
||||||
|
},
|
||||||
|
"php-http/promise": {
|
||||||
|
"version": "v1.0.0"
|
||||||
|
},
|
||||||
"phpdocumentor/reflection-common": {
|
"phpdocumentor/reflection-common": {
|
||||||
"version": "1.0.1"
|
"version": "1.0.1"
|
||||||
},
|
},
|
||||||
|
@ -98,9 +168,21 @@
|
||||||
"psr/container": {
|
"psr/container": {
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
|
"psr/http-client": {
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"psr/http-factory": {
|
||||||
|
"version": "1.0.1"
|
||||||
|
},
|
||||||
|
"psr/http-message": {
|
||||||
|
"version": "1.0.1"
|
||||||
|
},
|
||||||
"psr/log": {
|
"psr/log": {
|
||||||
"version": "1.0.2"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
|
"ralouphie/getallheaders": {
|
||||||
|
"version": "3.0.3"
|
||||||
|
},
|
||||||
"sensio/framework-extra-bundle": {
|
"sensio/framework-extra-bundle": {
|
||||||
"version": "4.0",
|
"version": "4.0",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
@ -110,6 +192,9 @@
|
||||||
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
|
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sentry/sdk": {
|
||||||
|
"version": "2.1.0"
|
||||||
|
},
|
||||||
"sentry/sentry": {
|
"sentry/sentry": {
|
||||||
"version": "1.10.0"
|
"version": "1.10.0"
|
||||||
},
|
},
|
||||||
|
@ -152,6 +237,9 @@
|
||||||
"symfony/dotenv": {
|
"symfony/dotenv": {
|
||||||
"version": "v4.1.0"
|
"version": "v4.1.0"
|
||||||
},
|
},
|
||||||
|
"symfony/error-handler": {
|
||||||
|
"version": "v4.4.2"
|
||||||
|
},
|
||||||
"symfony/event-dispatcher": {
|
"symfony/event-dispatcher": {
|
||||||
"version": "v4.1.0"
|
"version": "v4.1.0"
|
||||||
},
|
},
|
||||||
|
@ -188,6 +276,12 @@
|
||||||
"ref": "1279df12895f20d8076324036431833181eb6645"
|
"ref": "1279df12895f20d8076324036431833181eb6645"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"symfony/http-client": {
|
||||||
|
"version": "v4.4.2"
|
||||||
|
},
|
||||||
|
"symfony/http-client-contracts": {
|
||||||
|
"version": "v2.0.1"
|
||||||
|
},
|
||||||
"symfony/http-foundation": {
|
"symfony/http-foundation": {
|
||||||
"version": "v4.1.0"
|
"version": "v4.1.0"
|
||||||
},
|
},
|
||||||
|
@ -200,6 +294,18 @@
|
||||||
"symfony/intl": {
|
"symfony/intl": {
|
||||||
"version": "v4.1.0"
|
"version": "v4.1.0"
|
||||||
},
|
},
|
||||||
|
"symfony/mailer": {
|
||||||
|
"version": "4.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "master",
|
||||||
|
"version": "4.3",
|
||||||
|
"ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/mailer.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/mime": {
|
"symfony/mime": {
|
||||||
"version": "v4.3.2"
|
"version": "v4.3.2"
|
||||||
},
|
},
|
||||||
|
@ -236,6 +342,9 @@
|
||||||
"symfony/polyfill-php73": {
|
"symfony/polyfill-php73": {
|
||||||
"version": "v1.11.0"
|
"version": "v1.11.0"
|
||||||
},
|
},
|
||||||
|
"symfony/polyfill-uuid": {
|
||||||
|
"version": "v1.13.1"
|
||||||
|
},
|
||||||
"symfony/process": {
|
"symfony/process": {
|
||||||
"version": "v4.1.0"
|
"version": "v4.1.0"
|
||||||
},
|
},
|
||||||
|
@ -344,6 +453,9 @@
|
||||||
"twig/twig": {
|
"twig/twig": {
|
||||||
"version": "v2.4.8"
|
"version": "v2.4.8"
|
||||||
},
|
},
|
||||||
|
"webimpress/safe-writer": {
|
||||||
|
"version": "2.0.0"
|
||||||
|
},
|
||||||
"webmozart/assert": {
|
"webmozart/assert": {
|
||||||
"version": "1.3.0"
|
"version": "1.3.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
{% if invite.usedBy %}
|
{% if invite.usedBy %}
|
||||||
Used by <strong>{{ invite.usedBy.username }}</strong>.
|
Used by <strong>{{ invite.usedBy.username }}</strong>.
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set invite_url = url('user_register', { inviteCode: invite.code }) %}
|
{% set invite_url = url('user_register', { code: invite.code }) %}
|
||||||
<input class="form-control" type="url" value="{{ invite_url }}" readonly="readonly" onclick="this.select()">
|
<input class="form-control" type="url" value="{{ invite_url }}" readonly="readonly" onclick="this.select()">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -4,4 +4,6 @@
|
||||||
<div id="form-login">
|
<div id="form-login">
|
||||||
{{ form(form) }}
|
{{ form(form) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ path('user_reset_request') }}" class="btn btn-warning" role="button">Reset</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if inviteValid %}
|
{% if invite is not null %}
|
||||||
<div id="form-register">
|
<div id="form-register">
|
||||||
{{ form(form) }}
|
{{ form(form) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
11
templates/User/reset.html.twig
Normal file
11
templates/User/reset.html.twig
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if message is defined and message is not empty %}
|
||||||
|
<div class="alert alert-success" role="alert">{{ message }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="form-reset">
|
||||||
|
{{ form(form) }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -37,7 +37,7 @@
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<a href="{{ path('user_account_invites') }}" class="dropdown-item"><i class="fas fa-user-plus"></i> Invites</a>
|
<a href="{{ path('user_account_invites') }}" class="dropdown-item"><i class="fas fa-user-plus"></i> Invites</a>
|
||||||
<a href="{{ path('user_logout') }}" class="dropdown-item"><i class="fas fa-sign-out-alt"></i> Logout</a>
|
<a href="{{ path('user_auth_logout') }}" class="dropdown-item"><i class="fas fa-sign-out-alt"></i> Logout</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="well">
|
<div class="well">
|
||||||
{% if not is_granted('ROLE_USER') %}
|
{% if not is_granted('ROLE_USER') %}
|
||||||
<a href="{{ path('user_login') }}" class="btn btn-lg btn-primary"><i class="fas fa-sign-in-alt"></i> Login</a>
|
<a href="{{ path('user_auth_login') }}" class="btn btn-lg btn-primary"><i class="fas fa-sign-in-alt"></i> Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
Reference in a new issue