diff --git a/.gitignore b/.gitignore index 2b08932..48a881d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ /public/assets/ /assets/vendor/ ###< symfony/asset-mapper ### -./idea \ No newline at end of file +.idea \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 89c74d1..3bd1f7f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,10 +4,10 @@ services: database: image: postgres:${POSTGRES_VERSION:-16}-alpine environment: - POSTGRES_DB: ${POSTGRES_DB:-app} + POSTGRES_DB: ${POSTGRES_DB:-hegresphere} # You should definitely change the password in production - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!} - POSTGRES_USER: ${POSTGRES_USER:-app} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-Btssio2024} + POSTGRES_USER: ${POSTGRES_USER:-bourgoino} healthcheck: test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"] timeout: 5s @@ -23,3 +23,4 @@ volumes: ###> doctrine/doctrine-bundle ### database_data: ###< doctrine/doctrine-bundle ### + diff --git a/composer.json b/composer.json index 6cda857..d6015f0 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "symfony/validator": "7.1.*", "symfony/web-link": "7.1.*", "symfony/yaml": "7.1.*", + "symfonycasts/sass-bundle": "^0.7.0", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" }, diff --git a/composer.lock b/composer.lock index ab09ab1..8520c82 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": "1bc99d81c026aaf4e662ffffde457d04", + "content-hash": "39f4377209acc13ab9e977a5301faef0", "packages": [ { "name": "composer/semver", @@ -7344,6 +7344,61 @@ ], "time": "2024-09-17T12:49:58+00:00" }, + { + "name": "symfonycasts/sass-bundle", + "version": "v0.7.0", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/sass-bundle.git", + "reference": "d88601c50eff716d9273dffbd736adefdc19e2fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/sass-bundle/zipball/d88601c50eff716d9273dffbd736adefdc19e2fc", + "reference": "d88601c50eff716d9273dffbd736adefdc19e2fc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/console": "^5.4|^6.3|^7.0", + "symfony/filesystem": "^5.4|^6.3|^7.0", + "symfony/http-client": "^5.4|^6.3|^7.0", + "symfony/process": "^5.4|^6.3|^7.0" + }, + "require-dev": { + "matthiasnoback/symfony-config-test": "^5.0", + "phpstan/phpstan-symfony": "^1.4", + "symfony/framework-bundle": "^6.3|^7.0", + "symfony/phpunit-bridge": "^6.3|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfonycasts\\SassBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathéo Daninos", + "homepage": "https://github.com/WebMamba" + } + ], + "description": "Delightful Sass Support for Symfony + AssetMapper", + "keywords": [ + "asset-mapper", + "sass" + ], + "support": { + "issues": "https://github.com/SymfonyCasts/sass-bundle/issues", + "source": "https://github.com/SymfonyCasts/sass-bundle/tree/v0.7.0" + }, + "time": "2024-05-22T14:59:07+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.13.0", diff --git a/config/bundles.php b/config/bundles.php index 4e3a560..1ebcba9 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,4 +13,5 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfonycasts\SassBundle\SymfonycastsSassBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 6b3167f..ce8f2b7 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -16,6 +16,19 @@ security: main: lazy: true provider: app_user_provider + custom_authenticator: App\Security\UserAuthenticator + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route + + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: / + # by default, the feature is enabled by checking a checkbox in the + # login form, uncomment the following line to always enable it. + #always_remember_me: true # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..c77d5e2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,7 +4,7 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: - + app.jwtsecret : '%env(JWT_SECRET)%' services: # default configuration for services in *this* file _defaults: diff --git a/migrations/Version20241017125351.php b/migrations/Version20241017125351.php new file mode 100644 index 0000000..e9561a5 --- /dev/null +++ b/migrations/Version20241017125351.php @@ -0,0 +1,48 @@ +addSql('DROP SEQUENCE user_id_seq CASCADE'); + $this->addSql('CREATE SEQUENCE "userApp_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "userApp" (id INT NOT NULL, nickname VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, tel VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_NICKNAME ON "userApp" (nickname)'); + $this->addSql('ALTER TABLE employee DROP CONSTRAINT fk_5d9f75a1bf396750'); + $this->addSql('ALTER TABLE intern DROP CONSTRAINT fk_a5795f36bf396750'); + $this->addSql('DROP TABLE employee'); + $this->addSql('DROP TABLE "user"'); + $this->addSql('DROP TABLE intern'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE "userApp_id_seq" CASCADE'); + $this->addSql('CREATE SEQUENCE user_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE employee (id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, nickname VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, tel VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, mail VARCHAR(255) NOT NULL, discriminator VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX uniq_identifier_nickname ON "user" (nickname)'); + $this->addSql('CREATE TABLE intern (id INT NOT NULL, cover_letter TEXT NOT NULL, resume VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('ALTER TABLE employee ADD CONSTRAINT fk_5d9f75a1bf396750 FOREIGN KEY (id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE intern ADD CONSTRAINT fk_a5795f36bf396750 FOREIGN KEY (id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('DROP TABLE "userApp"'); + } +} diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000..88a4a84 --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,44 @@ +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 $security->login($user, UserAuthenticator::class, 'main'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form, + ]); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..25c2c56 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,103 @@ +getUser()) { + // return $this->redirectToRoute('target_path'); + // } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/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.'); + } + + #[Route('/mot-de-passe-oublie', name: 'forgotten_password')] + public function forgottenPassword( + Request $request, + UserRepository $userRepository, + JWTService $jwt, + SendEmailService $mail + ) : Response + { + $form = $this->createForm(ResetPasswordRequestFormType::class); + + $form->handleRequest($request); + + if($form->isSubmitted() && $form->isValid()) { + // Le formulaire est envoyé ET valide + // On va aller chercher l'utilisateur dans la base + $user = $userRepository->findOneByEmail($form->get('email')->getData()); + + // On verifie si on a un utilisateur + if($user) { + // On a un utilisateur + // On génère un JWT + // Générer le token + // Header + $header = [ + 'typ' => 'JWT', + 'alg' => 'HS256' + ]; + + //Payload + $payload = [ + 'user_id' => $user->getId() + ]; + + //On Génère le token + $token = $jwt->generate($header, $payload, $this->getParameter('app.jwtsecret')); + + // On génère l'URL vers reset_password + $url = $this->generateUrl('reset_password', ['token' => $token], + UrlGeneratorInterface::ABSOLUTE_URL); + + // Envoyer l'e-mail + $mail->send( + 'no-reply@openblog.test', + $user->getEmail(), + 'Récupération de votre mode de passe sur le site OpenBlog', + 'password_reset', + compact('user','url') // ['user' => $user, 'url'=>$url] + ); + + $this->addFlash('success', 'Email envoyé avec succès'); + return $this->redirectToRoute('app_login'); + + } + // $user est null + $this->addFlash('danger', 'Un problème est survenu'); + return $this->redirectToRoute('app_login'); + + } + + + return $this->render('security/reset_password_request.html.twig', ['requestPassForm' => $form->createView()]); + } + //#[Route('/mot-de-passe-oublie/{token}', name: 'reset_password')] + //public function resetPassword(): Response{ + + //}; +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 34c4b48..6706db8 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -4,12 +4,14 @@ namespace App\Entity; use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Entity(repositoryClass: UserRepository::class)] -#[ORM\Table(name: '`user`')] +#[ORM\Table(name: 'userApp')] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_NICKNAME', fields: ['nickname'])] +#[UniqueEntity(fields: ['nickname'], message: 'Il y a déjà un utilisateur à ce nom')] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] @@ -32,6 +34,21 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column] private ?string $password = null; + #[ORM\Column(length: 255)] + private ?string $firstName = null; + + #[ORM\Column(length: 255)] + private ?string $lastName = null; + + #[ORM\Column(length: 255)] + private ?string $tel = null; + + #[ORM\Column(length: 255)] + private ?string $address = null; + + #[ORM\Column(length: 255)] + private ?string $email = null; + public function getId(): ?int { return $this->id; @@ -106,4 +123,64 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface // If you store any temporary, sensitive data on the user, clear it here // $this->plainPassword = null; } -} + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + + return $this; + } + + public function getTel(): ?string + { + return $this->tel; + } + + public function setTel(string $tel): static + { + $this->tel = $tel; + + return $this; + } + + public function getAddress(): ?string + { + return $this->address; + } + + public function setAddress(string $address): static + { + $this->address = $address; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } +} \ No newline at end of file diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..9fe1e5a --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,56 @@ +add('nickname') + ->add('email') + ->add('agreeTerms', CheckboxType::class, [ + 'mapped' => false, + 'constraints' => [ + new IsTrue([ + 'message' => 'You should agree to our terms.', + ]), + ], + ]) + ->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' => 8, + '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/Form/ResetPasswordRequestFormType.php b/src/Form/ResetPasswordRequestFormType.php new file mode 100644 index 0000000..b92b33e --- /dev/null +++ b/src/Form/ResetPasswordRequestFormType.php @@ -0,0 +1,25 @@ +add('email', EmailType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // Configure your form options here + ]); + } +} diff --git a/src/Security/UserAuthenticator.php b/src/Security/UserAuthenticator.php new file mode 100644 index 0000000..a8d7224 --- /dev/null +++ b/src/Security/UserAuthenticator.php @@ -0,0 +1,60 @@ +getPayload()->getString('nickname'); + + $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $nickname); + + return new Passport( + new UserBadge($nickname), + new PasswordCredentials($request->getPayload()->getString('password')), + [ + new CsrfTokenBadge('authenticate', $request->getPayload()->getString('_csrf_token')), + new RememberMeBadge(), + ] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + + // For example: + return new RedirectResponse($this->urlGenerator->generate('index')); + //throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); + } + + protected function getLoginUrl(Request $request): string + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} diff --git a/symfony.lock b/symfony.lock index cf1900f..fdb5abe 100644 --- a/symfony.lock +++ b/symfony.lock @@ -288,6 +288,9 @@ "config/packages/messenger.yaml" ] }, + "symfonycasts/sass-bundle": { + "version": "v0.7.0" + }, "twig/extra-bundle": { "version": "v3.13.0" } diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig new file mode 100644 index 0000000..19b63c7 --- /dev/null +++ b/templates/registration/register.html.twig @@ -0,0 +1,27 @@ +{% extends 'base.html.twig' %} + +{% block title %}M'inscrire{% endblock %} + +{% block body %} +
Déjà inscrit(e) ? Me connecter
+{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..e7c0e1e --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,42 @@ +{% extends 'base.html.twig' %} + +{% block title %}Me connecter{% endblock %} + +{% block body %} + +{% endblock %} diff --git a/templates/security/reset_password_request.html.twig b/templates/security/reset_password_request.html.twig new file mode 100644 index 0000000..baba306 --- /dev/null +++ b/templates/security/reset_password_request.html.twig @@ -0,0 +1,14 @@ +{% extends 'base.html.twig' %} + +{% block title %}Demande de réinitialisation de mot de passe{% endblock %} + +{% block body %} +