diff --git a/assets/icons/material-symbols/logout.svg b/assets/icons/material-symbols/logout.svg new file mode 100644 index 0000000..d8d307d --- /dev/null +++ b/assets/icons/material-symbols/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/ri/user-line.svg b/assets/icons/ri/user-line.svg new file mode 100644 index 0000000..9c21bd6 --- /dev/null +++ b/assets/icons/ri/user-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/composer.json b/composer.json index 7a40359..e5d311f 100644 --- a/composer.json +++ b/composer.json @@ -91,6 +91,8 @@ } }, "require-dev": { - "symfony/maker-bundle": "^1.62" + "symfony/maker-bundle": "^1.62", + "symfony/stopwatch": "7.2.*", + "symfony/web-profiler-bundle": "7.2.*" } } diff --git a/composer.lock b/composer.lock index f3da352..a3994ae 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "740c22e45c004ece09230b2bb113e949", + "content-hash": "1b5e4263ced36558032c50f1d8f7f4fb", "packages": [ { "name": "1tomany/data-uri", @@ -8376,6 +8376,88 @@ } ], "time": "2025-01-15T00:21:40+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "4ffde1c860a100533b02697d9aaf5f45759ec26a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4ffde1c860a100533b02697d9aaf5f45759ec26a", + "reference": "4ffde1c860a100533b02697d9aaf5f45759ec26a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/config": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/form": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/serializer": "<7.2" + }, + "require-dev": { + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T14:27:24+00:00" } ], "aliases": [], diff --git a/config/bundles.php b/config/bundles.php index 0fa81a3..97ec940 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -16,4 +16,5 @@ return [ Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true], Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true], Symfony\UX\Turbo\TurboBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], ]; diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 7e1ee1f..4766a7a 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -5,6 +5,10 @@ framework: # Note that the session will be started ONLY if you read or write from it. session: true + trusted_proxies: 'private_ranges' + # trust *all* "X-Forwarded-*" headers + trusted_headers: [ 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix' ] + #esi: true #fragments: true diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..44d8a8a 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -5,13 +5,26 @@ security: # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: users_in_memory: { memory: null } + app_user_provider: + entity: + class: App\User\Framework\Entity\User + property: email + firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider + form_login: + login_path: app_login + check_path: app_login + enable_csrf: true + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -22,8 +35,8 @@ 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: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN, when@test: security: diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml new file mode 100644 index 0000000..1e039b7 --- /dev/null +++ b/config/packages/web_profiler.yaml @@ -0,0 +1,11 @@ +when@dev: + web_profiler: + toolbar: true + + framework: + profiler: + collect_serializer_data: true + +when@test: + framework: + profiler: { collect: false } diff --git a/config/routes/web_profiler.yaml b/config/routes/web_profiler.yaml new file mode 100644 index 0000000..8d85319 --- /dev/null +++ b/config/routes/web_profiler.yaml @@ -0,0 +1,8 @@ +when@dev: + web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + prefix: /_wdt + + web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + prefix: /_profiler diff --git a/migrations/Version20250428140450.php b/migrations/Version20250428140450.php new file mode 100644 index 0000000..959dc02 --- /dev/null +++ b/migrations/Version20250428140450.php @@ -0,0 +1,83 @@ +addSql(<<<'SQL' + CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL COMMENT '(DC2Type:json)', password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE episode DROP FOREIGN KEY FK_DDAA1CDAACB7A4A + SQL); + $this->addSql(<<<'SQL' + DROP TABLE processed_messages + SQL); + $this->addSql(<<<'SQL' + DROP TABLE episode + SQL); + $this->addSql(<<<'SQL' + DROP TABLE series + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE messenger_messages CHANGE id id BIGINT AUTO_INCREMENT NOT NULL + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at) + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + CREATE TABLE processed_messages (id INT AUTO_INCREMENT NOT NULL, run_id INT NOT NULL, attempt SMALLINT NOT NULL, message_type VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, description VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, dispatched_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', received_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', finished_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', wait_time BIGINT NOT NULL, handle_time BIGINT NOT NULL, memory_usage INT NOT NULL, transport VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, tags VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, failure_type VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, failure_message LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, results JSON DEFAULT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = '' + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE episode (id INT AUTO_INCREMENT NOT NULL, series_id_id INT DEFAULT NULL, imdb_id VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, tvdb_id VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, title VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, year VARCHAR(5) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, poster VARCHAR(500) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, season VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, episode VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, episode_code VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, download_directory VARCHAR(500) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, INDEX IDX_DDAA1CDAACB7A4A (series_id_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = '' + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE series (id INT AUTO_INCREMENT NOT NULL, imdb_id VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, tvdb_id VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, title VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, year VARCHAR(5) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, poster VARCHAR(500) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, directory VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, number_seasons INT DEFAULT NULL, number_episodes INT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = '' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE episode ADD CONSTRAINT FK_DDAA1CDAACB7A4A FOREIGN KEY (series_id_id) REFERENCES series (id) + SQL); + $this->addSql(<<<'SQL' + DROP TABLE user + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_75EA56E0FB7336F0 ON messenger_messages + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_75EA56E0E3BD61CE ON messenger_messages + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_75EA56E016BA31DB ON messenger_messages + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE messenger_messages CHANGE id id INT AUTO_INCREMENT NOT NULL + SQL); + } +} diff --git a/migrations/Version20250429020903.php b/migrations/Version20250429020903.php new file mode 100644 index 0000000..fe64772 --- /dev/null +++ b/migrations/Version20250429020903.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE user ADD name VARCHAR(255) DEFAULT 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 user DROP name + SQL); + } +} diff --git a/src/User/Framework/Controller/Web/LoginController.php b/src/User/Framework/Controller/Web/LoginController.php new file mode 100644 index 0000000..2f6bda4 --- /dev/null +++ b/src/User/Framework/Controller/Web/LoginController.php @@ -0,0 +1,32 @@ +getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('user/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + + #[Route(path: '/logout', name: 'app_logout')] + public function logout(): void + { + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/src/User/Framework/Controller/Web/RegistrationController.php b/src/User/Framework/Controller/Web/RegistrationController.php new file mode 100644 index 0000000..67276ec --- /dev/null +++ b/src/User/Framework/Controller/Web/RegistrationController.php @@ -0,0 +1,42 @@ +createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /** @var string $plainPassword */ + $plainPassword = $form->get('plainPassword')->getData(); + + // encode the plain password + $user->setPassword($userPasswordHasher->hashPassword($user, $plainPassword)); + + $entityManager->persist($user); + $entityManager->flush(); + + // do anything else you need here, like send an email + + return $this->redirectToRoute('app_index'); + } + + return $this->render('user/register.html.twig', [ + 'registrationForm' => $form, + ]); + } +} diff --git a/src/User/Framework/Entity/User.php b/src/User/Framework/Entity/User.php new file mode 100644 index 0000000..b617492 --- /dev/null +++ b/src/User/Framework/Entity/User.php @@ -0,0 +1,113 @@ +id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * The public representation of the user (e.g. a username, an email address, etc.) + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } +} diff --git a/src/User/Framework/Form/RegistrationFormType.php b/src/User/Framework/Form/RegistrationFormType.php new file mode 100644 index 0000000..8ce1a34 --- /dev/null +++ b/src/User/Framework/Form/RegistrationFormType.php @@ -0,0 +1,46 @@ +add('email') + ->add('name') + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/User/Framework/Repository/UserRepository.php b/src/User/Framework/Repository/UserRepository.php new file mode 100644 index 0000000..1760df3 --- /dev/null +++ b/src/User/Framework/Repository/UserRepository.php @@ -0,0 +1,60 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + +// /** +// * @return User[] Returns an array of User objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('u.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?User +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/symfony.lock b/symfony.lock index ba7b535..3359319 100644 --- a/symfony.lock +++ b/symfony.lock @@ -256,6 +256,19 @@ "config/packages/validator.yaml" ] }, + "symfony/web-profiler-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + }, "symfonycasts/tailwind-bundle": { "version": "0.10", "recipe": { diff --git a/templates/bare.html.twig b/templates/bare.html.twig new file mode 100644 index 0000000..5ce5297 --- /dev/null +++ b/templates/bare.html.twig @@ -0,0 +1,21 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {% block stylesheets %} + + {% endblock %} + + {% block javascripts %} + {% block importmap %}{{ importmap('app') }}{% endblock %} + {% endblock %} + + +

Torsearch

+
+ {% block body %}{% endblock %} +
+ + diff --git a/templates/components/Header.html.twig b/templates/components/Header.html.twig index 049e3de..6623b6c 100644 --- a/templates/components/Header.html.twig +++ b/templates/components/Header.html.twig @@ -6,7 +6,12 @@
diff --git a/templates/components/NavBar.html.twig b/templates/components/NavBar.html.twig index fac743e..58451b6 100644 --- a/templates/components/NavBar.html.twig +++ b/templates/components/NavBar.html.twig @@ -68,19 +68,17 @@ -
- - +
+ + + +

- Eric Frusciante + Eric Frusciante - eric@frusciante.com + eric@frusciante.com

diff --git a/templates/user/login.html.twig b/templates/user/login.html.twig new file mode 100644 index 0000000..8402c15 --- /dev/null +++ b/templates/user/login.html.twig @@ -0,0 +1,52 @@ +{% extends 'bare.html.twig' %} + +{% block title %}Log in — Torsearch{% endblock %} + +{% block body %} +
+

Login

+
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% if app.user %} +
+ You are logged in as {{ app.user.userIdentifier }}, Logout +
+ {% endif %} + + + + + + +
+ + +
+ + + +
+
+{% endblock %} diff --git a/templates/user/register.html.twig b/templates/user/register.html.twig new file mode 100644 index 0000000..e1c2505 --- /dev/null +++ b/templates/user/register.html.twig @@ -0,0 +1,43 @@ +{% extends 'bare.html.twig' %} + +{% block title %}Register{% endblock %} + +{% block body %} +
+

Register

+ + {{ form_errors(registrationForm) }} + + {{ form_start(registrationForm) }} + + + + + + + + + {{ form_end(registrationForm) }} +
+{% endblock %}