Password reset implemented, some refactoring, Sentry SDK and bundle upgraded, some routes changed. PHP requirements is up to 7.3.0.
This commit is contained in:
parent
578b6c35d1
commit
2c0c48d88e
5
.env
5
.env
|
@ -24,3 +24,8 @@ 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 ###
|
||||||
|
|
|
@ -12,17 +12,19 @@
|
||||||
],
|
],
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.2.0",
|
"php": "^7.3.0",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
|
"ext-hash": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"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",
|
||||||
|
|
1753
composer.lock
generated
1753
composer.lock
generated
File diff suppressed because it is too large
Load diff
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
|
||||||
|
|
|
@ -26,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%'
|
||||||
|
@ -44,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;
|
||||||
|
}
|
16
src/Form/Data/PasswordResetRequestData.php
Normal file
16
src/Form/Data/PasswordResetRequestData.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Data;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
class PasswordResetRequestData
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @Assert\Email()
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
*/
|
||||||
|
public $email;
|
||||||
|
}
|
|
@ -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
|
|
@ -21,6 +21,4 @@ class LoginType extends AbstractType
|
||||||
// Empty prefix for default UsernamePasswordFrormAuthenticationListener
|
// Empty prefix for default UsernamePasswordFrormAuthenticationListener
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
22
src/Form/PasswordResetRequestType.php
Normal file
22
src/Form/PasswordResetRequestType.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Form\Data\PasswordResetRequestData;
|
||||||
|
use Symfony\Component\Form\{AbstractType, Extension\Core\Type\EmailType, FormBuilderInterface};
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class PasswordResetRequestType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->add('email', EmailType::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,13 +2,13 @@
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
@ -23,8 +23,7 @@ class CreateUserRequestType extends AbstractType
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => CreateUserRequest::class,
|
'data_class' => RegisterData::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');
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
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,16 +42,24 @@ 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()) {
|
||||||
throw new InvalidInviteException();
|
throw new InvalidInviteException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $this->createUser($username, $password, $email,$roles);
|
$user = $this->createUser($username, $password, $email, $roles);
|
||||||
|
|
||||||
$invite->use($user);
|
$invite->use($user);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
81
symfony.lock
81
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": {
|
||||||
|
@ -62,6 +65,21 @@
|
||||||
"doctrine/reflection": {
|
"doctrine/reflection": {
|
||||||
"version": "v1.0.0"
|
"version": "v1.0.0"
|
||||||
},
|
},
|
||||||
|
"egulias/email-validator": {
|
||||||
|
"version": "2.1.14"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
|
@ -83,6 +101,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 +143,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 +167,9 @@
|
||||||
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
|
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sentry/sdk": {
|
||||||
|
"version": "2.1.0"
|
||||||
|
},
|
||||||
"sentry/sentry": {
|
"sentry/sentry": {
|
||||||
"version": "1.10.0"
|
"version": "1.10.0"
|
||||||
},
|
},
|
||||||
|
@ -191,6 +251,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"
|
||||||
},
|
},
|
||||||
|
@ -203,6 +269,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"
|
||||||
},
|
},
|
||||||
|
@ -239,6 +317,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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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