Compare commits

...

7 Commits

27 changed files with 690 additions and 14 deletions

View File

@@ -64,6 +64,14 @@ dialog[data-dialog-target="dialog"][closing] {
animation: fade-out 200ms forwards;
}
.text-input {
@apply bg-gray-50 text-gray-50 p-1 bg-transparent border-b-2 border-orange-400
}
.submit-button {
@apply bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40
}
.r-tablecell {
display: none;
}

View File

@@ -51,6 +51,7 @@
"symfony/ux-turbo": "^2.24",
"symfony/ux-twig-component": "^2.24",
"symfony/yaml": "7.3.*",
"symfonycasts/reset-password-bundle": "^1.23",
"symfonycasts/tailwind-bundle": "^0.10.0",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"

50
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a659d112da02d5ff1bde5081db62de31",
"content-hash": "f368fdd2e0f36d53131785b857200062",
"packages": [
{
"name": "1tomany/rich-bundle",
@@ -10257,6 +10257,54 @@
],
"time": "2025-04-04T10:10:33+00:00"
},
{
"name": "symfonycasts/reset-password-bundle",
"version": "v1.23.1",
"source": {
"type": "git",
"url": "https://github.com/SymfonyCasts/reset-password-bundle.git",
"reference": "bde42fe5956e0cd523931da886ee41ab660c45b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SymfonyCasts/reset-password-bundle/zipball/bde42fe5956e0cd523931da886ee41ab660c45b2",
"reference": "bde42fe5956e0cd523931da886ee41ab660c45b2",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=8.1.10",
"symfony/config": "^5.4 | ^6.0 | ^7.0",
"symfony/dependency-injection": "^5.4 | ^6.0 | ^7.0",
"symfony/deprecation-contracts": "^2.2 | ^3.0",
"symfony/http-kernel": "^5.4 | ^6.0 | ^7.0"
},
"require-dev": {
"doctrine/annotations": "^1.0",
"doctrine/doctrine-bundle": "^2.8",
"doctrine/orm": "^2.13",
"symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0",
"symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0",
"symfony/process": "^6.4 | ^7.0 | ^7.1",
"symfonycasts/internal-test-helpers": "dev-main"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"SymfonyCasts\\Bundle\\ResetPassword\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Symfony bundle that adds password reset functionality.",
"support": {
"issues": "https://github.com/SymfonyCasts/reset-password-bundle/issues",
"source": "https://github.com/SymfonyCasts/reset-password-bundle/tree/v1.23.1"
},
"time": "2024-12-09T19:04:36+00:00"
},
{
"name": "symfonycasts/tailwind-bundle",
"version": "v0.10.0",

View File

@@ -20,4 +20,5 @@ return [
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
];

View File

@@ -18,6 +18,12 @@ doctrine:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
# App:
# type: attribute
# is_bundle: false
# dir: '%kernel.project_dir%/src/Entity'
# prefix: 'App\Entity'
# alias: App
Download:
type: attribute
is_bundle: false

View File

@@ -0,0 +1,2 @@
symfonycasts_reset_password:
request_password_repository: App\User\Framework\Repository\ResetPasswordRequestRepository

View File

@@ -36,6 +36,7 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }

View File

@@ -43,7 +43,7 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709200956 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE reset_password_request (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', expires_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_7CE748AA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE monitor CHANGE only_future only_future TINYINT(1) NOT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE reset_password_request DROP FOREIGN KEY FK_7CE748AA76ED395
SQL);
$this->addSql(<<<'SQL'
DROP TABLE reset_password_request
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE monitor CHANGE only_future only_future TINYINT(1) DEFAULT 1 NOT NULL
SQL);
}
}

View File

@@ -11,7 +11,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'config:set',
description: 'Add a short description for your command',
description: '[deprecated] This command currently serves no use. It may be re-purposed or removed in the future.',
)]
class ConfigSetCommand extends Command
{

View File

@@ -15,7 +15,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'db:seed',
description: 'Seed the database with required data.',
description: 'Seeds the database with required data. This command is run every time a new container is created from the torsearch-app image and is part of the init process.',
)]
class SeedDatabaseCommand extends Command
{

View File

@@ -11,7 +11,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'startup:status',
description: 'Add a short description for your command',
description: 'Used by the Docker healthcheck system to signal when the container is healthy.',
)]
class StartupStatusCommand extends Command
{

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Base\Framework\Command;
use App\User\Framework\Repository\UserRepository;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[AsCommand(name: 'user:reset-password', description: 'Resets the password for the given user. Requires either the ID or email of the User. You will be asked for the password after running the command.')]
class UserResetPasswordCommand extends Command
{
private readonly Security $security;
private readonly UserRepository $userRepository;
private readonly UserPasswordHasherInterface $hasher;
public function __construct(
Security $security,
UserRepository $userRepository,
UserPasswordHasherInterface $hasher,
) {
parent::__construct();
$this->security = $security;
$this->userRepository = $userRepository;
$this->hasher = $hasher;
}
protected function configure(): void
{
$this
->addOption('id', null, InputOption::VALUE_REQUIRED, 'The ID of the user in the database.')
->addOption('email', null, InputOption::VALUE_REQUIRED, 'The email of the user.')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$queryParams = $this->parseInput($input, $io);
if ([] === $queryParams) {
$io->error('No ID or Email specified. Please run again and pass the "--id" or "--email" option.');
return Command::FAILURE;
}
$user = $this->userRepository->findOneBy($queryParams);
if (null === $user) {
$io->error('No such user exists.');
return Command::FAILURE;
}
try {
$newPassword = $this->askForPassword($input, $output);
$this->updateUsersPassword($user, $newPassword);
} catch (\Throwable $exception) {
$io->error($exception->getMessage());
return Command::FAILURE;
}
$io->success('Success. The password has been reset.');
return Command::SUCCESS;
}
private function parseInput(InputInterface $input, SymfonyStyle $io): array
{
if ($input->getOption('id')) {
return ['id' => $input->getOption('id')];
} elseif ($input->getOption('email')) {
return ['email' => $input->getOption('email')];
}
return [];
}
private function askForPassword(InputInterface $input, OutputInterface $output): ?string
{
$questionHelper = new QuestionHelper();
$question = new Question('New password (input is hidden): ')
->setHidden(true)
->setHiddenFallback(false)
->setNormalizer(function (?string $value): string {
return $value ?? '';
})
->setValidator(function (string $value): string {
if ('' === trim($value)) {
throw new \Exception('The password cannot be empty');
}
return $value;
})
->setMaxAttempts(5)
;
return $questionHelper->ask($input, $output, $question);
}
private function updateUsersPassword(UserInterface $user, string $newPassword): void
{
$user->setPassword(
$this->hasher->hashPassword($user, $newPassword)
);
$this->userRepository->getEntityManager()->flush();
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace App\User\Framework\Controller\Web;
use App\User\Framework\Entity\User;
use App\User\Framework\Form\ChangePasswordForm;
use App\User\Framework\Form\ResetPasswordRequestForm;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
#[Route('/reset-password')]
class ResetPasswordController extends AbstractController
{
use ResetPasswordControllerTrait;
public function __construct(
private ResetPasswordHelperInterface $resetPasswordHelper,
private EntityManagerInterface $entityManager,
private readonly Security $security
) {
}
/**
* Display & process form to request a password reset.
*/
#[Route('', name: 'app_forgot_password_request')]
public function request(
Request $request,
MailerInterface $mailer,
LoggerInterface $logger
): Response {
$form = $this->createForm(ResetPasswordRequestForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var string $email */
$email = $form->get('email')->getData();
return $this->processSendingPasswordResetEmail($email, $mailer, $logger);
}
return $this->render('user/reset_password/request.html.twig', [
'requestForm' => $form,
]);
}
/**
* Confirmation page after a user has requested a password reset.
*/
#[Route('/check-email', name: 'app_check_email')]
public function checkEmail(): Response
{
// Generate a fake token if the user does not exist or someone hit this page directly.
// This prevents exposing whether or not a user was found with the given email address or not
if (null === ($resetToken = $this->getTokenObjectFromSession())) {
$resetToken = $this->resetPasswordHelper->generateFakeResetToken();
}
return $this->render('user/reset_password/check_email.html.twig', [
'resetToken' => $resetToken,
]);
}
/**
* Validates and process the reset URL that the user clicked in their email.
*/
#[Route('/reset/{token}', name: 'app_reset_password')]
public function reset(
Request $request,
UserPasswordHasherInterface $passwordHasher,
TranslatorInterface $translator,
?string $token = null
): Response {
if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
$this->storeTokenInSession($token);
return $this->redirectToRoute('app_reset_password');
}
$token = $this->getTokenFromSession();
if (null === $token) {
throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
}
try {
/** @var User $user */
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
$this->addFlash('reset_password_error', sprintf(
'%s - %s',
$translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, [], 'ResetPasswordBundle'),
$translator->trans($e->getReason(), [], 'ResetPasswordBundle')
));
return $this->redirectToRoute('app_forgot_password_request');
}
// The token is valid; allow the user to change their password.
$form = $this->createForm(ChangePasswordForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// A password reset token should be used only once, remove it.
$this->resetPasswordHelper->removeResetRequest($token);
/** @var string $plainPassword */
$plainPassword = $form->get('plainPassword')->getData();
// Encode(hash) the plain password, and set it.
$user->setPassword($passwordHasher->hashPassword($user, $plainPassword));
$this->entityManager->flush();
// The session is cleaned up after the password has been changed.
$this->cleanSessionAfterReset();
return $this->redirectToRoute('app_index');
}
return $this->render('user/reset_password/reset.html.twig', [
'resetForm' => $form,
]);
}
private function processSendingPasswordResetEmail(
string $emailFormData,
MailerInterface $mailer,
LoggerInterface $logger
): RedirectResponse {
$user = $this->entityManager->getRepository(User::class)->findOneBy([
'email' => $emailFormData,
]);
// Do not reveal whether a user account was found or not.
if (!$user) {
return $this->redirectToRoute('app_check_email');
}
try {
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
} catch (ResetPasswordExceptionInterface $e) {
$logger->error('> [ResetPasswordController@processSendingPasswordResetEmail] ' . $e->getMessage());
$this->addFlash(
'reset_password_error',
'Your password reset token could not be generated. If you\'re the system administrator, check the server logs for more details.'
);
return $this->redirectToRoute('app_check_email');
}
$email = (new TemplatedEmail())
->to((string) $user->getEmail())
->subject('Your password reset request')
->htmlTemplate('user/reset_password/email.html.twig')
->context([
'resetToken' => $resetToken,
])
;
$mailer->send($email);
// Store the token object in session for retrieval in check-email route.
$this->setTokenObjectInSession($resetToken);
return $this->redirectToRoute('app_check_email');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\User\Framework\Entity;
use App\User\Framework\Repository\ResetPasswordRequestRepository;
use Doctrine\ORM\Mapping as ORM;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
#[ORM\Entity(repositoryClass: ResetPasswordRequestRepository::class)]
class ResetPasswordRequest implements ResetPasswordRequestInterface
{
use ResetPasswordRequestTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
public function __construct(User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
{
$this->user = $user;
$this->initialize($expiresAt, $selector, $hashedToken);
}
public function getId(): ?int
{
return $this->id;
}
public function getUser(): User
{
return $this->user;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\User\Framework\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
use Symfony\Component\Validator\Constraints\PasswordStrength;
class ChangePasswordForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
'options' => [
'attr' => [
'autocomplete' => 'new-password',
'class' => 'text-input w-full mb-4'
],
'label_attr' => [
'class' => 'block'
]
],
'first_options' => [
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 12,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
new PasswordStrength(),
new NotCompromisedPassword(),
],
'label' => 'New password',
],
'second_options' => [
'label' => 'Repeat Password',
],
'invalid_message' => 'The password fields must match.',
// Instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\User\Framework\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class ResetPasswordRequestForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class, [
'attr' => ['autocomplete' => 'email'],
'constraints' => [
new NotBlank([
'message' => 'Please enter your email',
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([]);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\User\Framework\Repository;
use App\User\Framework\Entity\ResetPasswordRequest;
use App\User\Framework\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait;
use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface;
/**
* @extends ServiceEntityRepository<ResetPasswordRequest>
*/
class ResetPasswordRequestRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface
{
use ResetPasswordRequestRepositoryTrait;
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ResetPasswordRequest::class);
}
/**
* @param User $user
*/
public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface
{
return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken);
}
}

View File

@@ -375,6 +375,18 @@
"config/routes/web_profiler.yaml"
]
},
"symfonycasts/reset-password-bundle": {
"version": "1.23",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "97c1627c0384534997ae1047b93be517ca16de43"
},
"files": [
"config/packages/reset_password.yaml"
]
},
"symfonycasts/tailwind-bundle": {
"version": "0.10",
"recipe": {

View File

@@ -15,8 +15,12 @@
</head>
<body class="bg-cyan-950 flex flex-col h-full">
<h1 class="px-4 py-4 text-3xl font-extrabold text-orange-500">Torsearch</h1>
<div class="flex flex-col justify-center items-center">
<div class="p-4 flex flex-col justify-center items-center">
{% block body %}{% endblock %}
<div class="mt-2 inline-flex gap-4 justify-between text-white">
<a class="text-sm" href="{{ path('app_login') }}">Sign In</a>
<span class="text-sm">v{{ version }}</span>
</div>
</div>
</body>
</html>

View File

@@ -9,6 +9,6 @@
mediaType: mediaType,
imdbId: imdbId
}) }}">
<h3 class="text-center text-white text-xl md:text-base md:max-w-[16ch]">{{ title }}</h3>
<h3 class="text-center text-white md:text-xl md:text-base md:max-w-[16ch]">{{ title }}</h3>
</a>
</div>

View File

@@ -20,7 +20,7 @@
</twig:Card>
</div>
<div class="flex flex-col gap-4">
<twig:Card title="Popular Movies" contentClass="flex flex-col gap-4 md:flex-row md:justify-between w-full">
<twig:Card title="Popular Movies" contentClass="grid grid-cols-2 gap-4 md:flex md:flex-row md:justify-between w-full">
{% for movie in popular_movies %}
<twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}"
@@ -32,7 +32,7 @@
/>
{% endfor %}
</twig:Card>
<twig:Card title="Popular TV Shows" contentClass="flex flex-col md:flex-row justify-between w-full">
<twig:Card title="Popular TV Shows" contentClass="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
{% for movie in popular_tvshows %}
<twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}"

View File

@@ -3,7 +3,7 @@
{% block title %}Log in &mdash; Torsearch{% endblock %}
{% block body %}
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 min-w-96 border-orange-500 border-2 text-gray-50">
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50">
<h2 class="text-xl font-bold">Login</h2>
<form method="post" class="flex flex-col gap-2">
{% if error %}
@@ -40,14 +40,22 @@
</label>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" data-controller="csrf-protection">
<div class="mb-2">
<input type="checkbox" name="_remember_me" id="_remember_me">
<label for="_remember_me">Remember me</label>
</div>
<div class="mb-2 flex flex-row justify-between">
<div>
<input type="checkbox" name="_remember_me" id="_remember_me">
<label for="_remember_me">Remember me</label>
</div>
</div>
<button type="submit" class="bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40">
Sign in
</button>
<div class="flex">
<a href="{{ path('app_forgot_password_request') }}">Forgot password?</a>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'bare.html.twig' %}
{% block title %}Password Reset Email Sent{% endblock %}
{% block body %}
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50">
<h2 class="text-xl font-bold">Head over to your email</h2>
<div class="mb-3 flex flex-col gap-4">
<p>
If an account matching your email exists, then an email was just sent that contains a
link that you can use to reset your password. This link will expire in
{{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.
</p>
<p>
If you don't receive an email please check your spam folder or
<a href="{{ path('app_forgot_password_request') }}">try again</a>.
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
<h1>Hi!</h1>
<p>To reset your password, please visit the following link</p>
<a href="{{ url('app_reset_password', {token: resetToken.token}) }}">{{ url('app_reset_password', {token: resetToken.token}) }}</a>
<p>This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
<p>Cheers!</p>

View File

@@ -0,0 +1,32 @@
{% extends 'bare.html.twig' %}
{% block title %}Reset your password &mdash; Torsearch{% endblock %}
{% block body %}
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50">
<h2 class="text-xl font-bold">Reset your password</h2>
<div class="mb-3">
Enter your email address, and we'll send you a link to reset your password.
</div>
<form name="reset_password_request_form" method="post" class="flex flex-col gap-2">
{% for flash_error in app.flashes('reset_password_error') %}
<div class="mb-3 p-2 bg-rose-500 text-black text-semibold rounded-md" role="alert">{{ flash_error }}</div>
{% endfor %}
<label for="reset_password_request_form_email" class="required flex flex-col mb-2">
Email
<input type="email"
class="text-input"
id="reset_password_request_form_email"
name="reset_password_request_form[email]"
required="required" autocomplete="email">
</label>
<input type="hidden" id="reset_password_request_form__token" name="reset_password_request_form[_token]" data-controller="csrf-protection" value="csrf-token">
<button class="submit-button">Send password reset email</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'bare.html.twig' %}
{% block title %}Reset your password &mdash; Torsearch{% endblock %}
{% block body %}
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50">
<h2 class="text-xl font-bold text-white">Reset your password</h2>
<div class="mb-2">
Enter a new password for your account.
</div>
{{ form_start(resetForm) }}
{{ form_row(resetForm.plainPassword) }}
<button class="submit-button">Reset password</button>
{{ form_end(resetForm) }}
</div>
{% endblock %}