Appli de gestion de mot de passe en Symfony et Vue.js (Partie 1)

Le tutorial du jour va être de développer une application qui servira à stocker vos nombreux mots de passe. (site internet, numéro CB, code PIN, etc…)
Elle portera le doux nom de Gespass.
Cette application servira uniquement d’exercice, je ne vous conseille pas de l’utiliser en production sans en améliorer la sécurité (par exemple au moins ajouter une authentification .htaccess)

Mon environnement de dev actuel :

  • Mint 19 xfce
  • php 7.2.5
  • mariadb 10.1.29
  • symfony 4.1.1
  • server web de symfony
  • vue.js 2.5.16

Cet article sera écrit en 3 parties :

Vous pouvez retrouver le projet complet ici : https://github.com/gponty/gespassvuejs

On va d’abord créer le projet symfony :

composer create-project symfony/skeleton gespass

Et installer tous les bundles que nous allons avoir besoin :

# Serveur symfony
composer require symfony/web-server-bundle --dev
# Gestion des données
composer require symfony/orm-pack
# Aide à la creation des entites, formulaires, etc...
composer require symfony/maker-bundle --dev
# Gestion des formulaires
composer require symfony/form
# Gestion de twig
composer require symfony/templating
# debug
composer require symfony/debug --dev
composer require profiler --dev
# Asset
composer require symfony/asset
# fichiers logs
composer require symfony/monolog-bundle
# validator
composer require symfony/validator
# Serializer
composer require symfony/serializer

On lance le serveur pour voir que tout fonctionne

php bin/console server:run

En allant à http://127.0.0.1:8000 (le numéro de port peut être différent si il est déjà pris), vous devriez voir une velle page symfony :

On va tout de suite mettre à jour les identifiants de notre base dans le fichier.env : (vous remarquerez que sur ma machine locale je n’ai pas besoin de mot de passe pour me connecter à mariaDB)

DATABASE_URL=mysql://root:@127.0.0.1:3306/gespass

Et on créé notre base dans la foulée :

php bin/console doctrine:database:create
Created database `gespass` for connection named default

On va ensuite créer nos tables, enfin plutôt notre table, puisque il y en aura q’une seule, celle qui contiendra l’ensemble des mots de passe, on l’appellera simplement : Password

On va utiliser pour cela le « maker-bundle » :

php bin/console make:entity

C’est relativement simple, il suffit simplement de répondre aux questions, ce qui va nous donner au final :

<?php
// /src/Entity/MotDePasse.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\MotDePasseRepository")
 */
class MotDePasse
{

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $titre;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $url;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $note;

    public function getId()
    {
        return $this->id;
    }

    public function getTitre(): ?string
    {
        return $this->titre;
    }

    public function setTitre(string $titre): self
    {
        $this->titre = $titre;

        return $this;
    }

    public function getUrl(): ?string
    {
        return $this->url;
    }

    public function setUrl(?string $url): self
    {
        $this->url = $url;

        return $this;
    }

    public function getUsername(): ?string
    {
        return $this->username;
    }

    public function setUsername(?string $username): self
    {
        $this->username = $username;

        return $this;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

    public function setPassword(?string $password): self
    {
        $this->password = $password;

        return $this;
    }

    public function getNote(): ?string
    {
        return $this->note;
    }

    public function setNote(?string $note): self
    {
        $this->note = $note;

        return $this;
    }

}

On va modifier légèrement cette entity en utilisant le principe des « traits » (je ferai sûrement un article plus tard pour détailler ce principe)

Même si pour ce projet ce n’est pas indispensable, car nous n’aurons qu’une entité.

On va créer un fichier TimestampableEntity.php qui contiendra ceci :

<?php
/src/Entity/TimestampableEntity.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

trait TimestampableEntity
{
    /**
     * @var \DateTime
     *
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $createdAt;

    /**
     * @var \DateTime
     *
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $updatedAt;

    /**
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * @param \DateTime $createdAt
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;
    }

    /**
     * @return \DateTime
     */
    public function getUpdatedAt()
    {
        return $this->updatedAt;
    }

    /**
     * @param \DateTime $updatedAt
     */
    public function setUpdatedAt($updatedAt)
    {
        $this->updatedAt = $updatedAt;
    }

    /**
     * @ORM\PrePersist
     * @ORM\PreUpdate
     */
    public function updatedTimestamps()
    {
        $this->setUpdatedAt(new \DateTime('now'));

        if ($this->getCreatedAt() == null) {
            $this->setCreatedAt(new \DateTime('now'));
        }
    }
}

Et on va ajouter dans le fichier MotDePasse.php :

use TimestampableEntity;

et dire que notre entité contient des « callbacks de cycle de vie » :

* @ORM\HasLifecycleCallbacks

On peut maintenant créer la base (comme pour les « traits », je ferai aussi un article sur le principe de migration) :

php bin/console make:migration

php bin/console doctrine:migrations:migrate

Le résultat :

hesiode@hesiode-MS-7917:/var/www/gespass$ php bin/console make:migration


Success! 

Next: Review the new migration "src/Migrations/Version20180706115618.php"
Then: Run the migration with php bin/console doctrine:migrations:migrate
See https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html

hesiode@hesiode-MS-7917:/var/www/gespass$ php bin/console doctrine:migrations:migrate

Application Migrations 

WARNING! You are about to execute a database migration that could result in schema changes and data loss. Are you sure you wish to continue? (y/n)y
Migrating up to 20180706115618 from 0

++ migrating 20180706115618

-> CREATE TABLE password (id INT AUTO_INCREMENT NOT NULL, titre VARCHAR(255) NOT NULL, url VARCHAR(255) DEFAULT NULL, username VARCHAR(255) DEFAULT NULL, mot_de_passe VARCHAR(255) DEFAULT NULL, note VARCHAR(255) DEFAULT NULL, created_at DATETIME DEFAULT NULL, updated_at DATETIME DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB

++ migrated (0.02s)

------------------------

++ finished in 0.02s
++ 1 migrations executed
++ 1 sql queries

On va aussi créer notre controller principal, on va aussi profiter du maker :

hesiode@hesiode-MS-7917:/var/www/gespass$ php bin/console make:controller

 Choose a name for your controller class (e.g. GentleGnomeController):
 > DefaultController

 created: src/Controller/DefaultController.php
 created: templates/default/index.html.twig

           
  Success! 
           

 Next: Open your new controller class and add some pages!

Dans le fichier généré (/src/Controller/DefaultController.php)

On va remplacer la route « /default » par « / ».

Et on peut voir la page d’accueil qui va changer :

Sur le même principe, on va en profiter pour créer le controller qui va gérer les mots de passe, il s’appellera simplement MotDePasseController.php et contiendra 5 fonctions :

  • /passwords : Retourne la liste complète des mots de passe
  • /motdepasse/new : Sauvegarde dans la base un nouveau mot de passe
  • /motdepasse/save/{id} : Sauvegarde après modif un mot de passe existant
  • /motdepasse/delete/{id} : Supprime un mot de passe de la base
  • /motdepasse/generate : Génère un mot de passe aléatoire

Ce qui nous donne :

<?php

namespace App\Controller;

use App\Entity\MotDePasse;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

class MotDePasseController extends Controller
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var \Doctrine\Common\Persistence\ObjectRepository
     */
    private $passwordRepository;

    /**
     * ProjectController constructor.
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->passwordRepository = $entityManager->getRepository('App:MotDePasse');
    }

    /**
     * Retourne la liste de tous les mots de passe
     * @Route("/passwords", name="password_liste")
     */
    public function index()
    {
        $password = $this->passwordRepository->findAll();

        $jsonContent = $this->serializeObject($password);

        return new Response($jsonContent, Response::HTTP_OK);
    }

    /**
     * Sauvegarde un nouveau mot de passe
     * @param Request $request
     * @return Response
     * @Route("/motdepasse/new", name="password_new")
     */
    public function newPassword(Request $request)
    {
        $content = json_decode($request->getContent(), true);

        if ($content['titre']) {

            $pass = new MotDePasse();
            $pass->setTitre($content['titre']);
            $pass->setPassword($content['motDePasse']);
            $pass->setUsername($content['username']);
            $pass->setNote($content['note']);
            $pass->setUrl($content['url']);
            $this->entityManager->persist($pass);
            $this->entityManager->flush();

            // Serialize object into Json format
            $jsonContent = $this->serializeObject($pass);

            return new Response($jsonContent, Response::HTTP_OK);
        }

        return new Response('Error', Response::HTTP_INTERNAL_SERVER_ERROR);

    }

    /**
     * Sauvegarde un mot de passe existant
     * @param Request $request
     * @param MotDePasse $id
     * @return Response
     * @Route("/motdepasse/save/{id}", name="password_edit")
     */
    public function savePassword(Request $request, $id)
    {
        $content = json_decode($request->getContent(), true);

        $pass = $this->passwordRepository->find($id);

        if ($pass && $content['titre']) {

            $pass->setTitre($content['titre']);
            if (isset($content['motDePasse'])) $pass->setPassword($content['motDePasse']);
            if (isset($content['username'])) $pass->setUsername($content['username']);
            if (isset($content['note'])) $pass->setNote($content['note']);
            if (isset($content['url'])) $pass->setUrl($content['url']);
            $this->entityManager->flush();

            // Serialize object into Json format
            $jsonContent = $this->serializeObject($pass);

            return new Response($jsonContent, Response::HTTP_OK);
        } else {
            return new Response('Error', Response::HTTP_INTERNAL_SERVER_ERROR);
        }

    }

    /**
     * Supprime un mot de passe
     * @param Request $request
     * @param MotDePasse $id
     * @return Response
     * @Route("/motdepasse/delete/{id}", name="password_delete")
     */
    public function deletePassword(Request $request, $id)
    {

        $pass = $this->passwordRepository->find($id);

        $this->entityManager->remove($pass);
        $this->entityManager->flush();

        return new Response('ok', Response::HTTP_OK);

    }

    /**
     * Génére un mot de passe aleatoire
     * @param Request $request
     * @return Response
     * @Route("/motdepasse/generate", name="password_generate")
     */
    public function generatePassword(Request $request)
    {
        $mot_de_passe = "";

        $chaine = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ023456789+@!$%?&";
        $longeur_chaine = strlen($chaine);
        $nb_caractere = 8;
        for ($i = 1; $i <= $nb_caractere; $i++) {
            $place_aleatoire = mt_rand(0, ($longeur_chaine - 1));
            $mot_de_passe .= $chaine[$place_aleatoire];
        }
        return new Response($mot_de_passe, Response::HTTP_OK);

    }

    // Serialize l'entité
    public function serializeObject($object)
    {
        $encoders = new JsonEncoder();
        $normalizers = new ObjectNormalizer();

        $normalizers->setCircularReferenceHandler(function ($obj) {
            return $obj->getId();
        });
        $serializer = new Serializer(array($normalizers), array($encoders));

        $jsonContent = $serializer->serialize($object, 'json');

        return $jsonContent;
    }
}

Tout est maintenant prêt pour installer et configurer vue.js, il suffit d’exécuter ces commandes :

# Installation de vue-js

yarn add --dev vue vue-loader@^14.2.2 vue-template-compiler

# Installation de webpack qui va nous servir à (entre autre) compiler nos assets (css et js)

yarn add --dev @symfony/webpack-encore

# installation de bootstrap (bibliothèque css) et jquery (bibliothèque javascript)

yarn add sass-loader node-sass jquery bootstrap-sass --dev

# installation de axios (qui va nous permettre d'interroger symfony)
# et moment pour gérer les dates en javascript

yarn add moment axios

Dans le prochain chapitre, on configurera vue.js !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.