Skip to content

Étape 16 – Ajouter une page de connexion et protéger certaines routes

🎯 Objectifs

  • Créer une page de connexion fonctionnelle
  • Utiliser le store d’authentification fourni (authStore.js)
  • Ajouter des boutons dynamiques dans le menu (connexion / déconnexion)
  • Protéger certaines pages (comme /pokemons/create) en exigeant d’être connecté

📦 Ce que vous avez déjà

ÉlémentStatutAction à faire
authStore.jsDéjà fourni ✅À importer et utiliser
router/index.jsBasique 🟡À modifier
Login.vueÀ créer 🔧Suivre l’exemple ci-dessous
AppHeader.vueÀ modifier 🔧Ajouter les boutons

📝 1. Créer la page src/pages/Login.vue

vue
<template>
  <!-- Conteneur principal pour centrer la feuille de connexion -->
  <v-container>
    <!--
    Feuille de connexion
      * v-sheet : Conteneur Vuetify stylisé.
      * class="mx-auto bg-transparent" :
        - mx-auto : Centre horizontalement la feuille.
        - bg-transparent : Fond transparent.
      * max-width="400" : Définit une largeur maximale de 400px.
    -->
    <v-sheet class="mx-auto bg-transparent" max-width="400">
      <!-- Titre de la page -->
      <h1 class="mb-4">Connexion</h1>
      <!--
      Informations de connexion
        * v-sheet : Conteneur Vuetify stylisé.
        * class="pa-2 my-4" : Ajoute une marge intérieure partout (pa) et d'une marge extérieure en haut et en bas (my).
      -->
      <v-sheet class="pa-2 my-4">
        email: <code>sacha@pokemon.com</code> <br>
        password: <code>pika</code>
      </v-sheet>

      <!--
      Formulaire de connexion
        * v-form : Composant Vuetify pour gérer les validations et soumissions de formulaire.
        * @submit.prevent="login" : Intercepte l'envoi pour appeler la méthode `login`.
      -->
      <v-form @submit.prevent="login">
        <!--
        Champ email
          * v-text-field : Composant de champ de texte Vuetify.
          * v-model="loginEmail" : Liaison bidirectionnelle avec la donnée `loginEmail`.
          * aria-label : Améliore l'accessibilité pour les lecteurs d'écran.
          * label="Email" : Affiche un label pour le champ.
          * required : Rend le champ obligatoire.
          * :rules="[validateEmail]" : Applique des règles de validation spécifiques.
          * type="email" : Définit le type du champ pour les navigateurs modernes.
        -->
        <v-text-field
            v-model="loginEmail"
            aria-label="Champ de saisie pour l'email"
            autofocus
            label="Email"
            required
            :rules="[validateEmail]"
            type="email"
        />
        <!--
        Champ mot de passe
          * Similaire au champ email, mais utilise `loginPassword` et une règle de validation différente.
        -->
        <v-text-field
            v-model="loginPassword"
            aria-label="Champ de saisie pour le mot de passe"
            label="Mot de passe"
            required
            :rules="[validatePassword]"
            type="password"
        />

        <!--
        Message d'erreur
          * v-alert : Affiche un message d'avertissement si `errorMessage` est défini.
          * v-if="errorMessage" : Affiche uniquement si une erreur est présente.
          * border="start" : Ajoute une bordure à gauche.
          * class="mb-6" : Ajoute une marge inférieure pour espacer le message.
          * color="warning" : Définit une couleur d'alerte jaune.
        -->
        <v-alert
            v-if="errorMessage"
            border="start"
            class="mb-6"
            color="warning"
        >
          {{ errorMessage }}
        </v-alert>

        <!--
        Bouton de connexion
          * v-btn : Bouton Vuetify.
          * aria-label : Accessibilité pour lecteurs d'écran.
          * color="primary" : Style le bouton avec une couleur principale.
          * size="large" : Définit une taille large pour le bouton.
          * type="submit" : Définit le bouton comme déclencheur de soumission du formulaire.
        -->
        <v-btn
            aria-label="Bouton pour se connecter"
            color="primary"
            size="large"
            type="submit"
        >Se connecter</v-btn>
      </v-form>
    </v-sheet>
  </v-container>
</template>

<script setup>
  // Importation des dépendances nécessaires
  import { useAuthStore } from '@/stores/authStore'
  import { useRoute, useRouter } from 'vue-router'
  import { ref } from 'vue'

  // Initialisation du router et de la route pour la redirection
  const router = useRouter()
  const route = useRoute()

  // Store d'authentification
  const authStore = useAuthStore()

  // Données réactives pour le formulaire
  const loginEmail = ref('')
  const loginPassword = ref('')

  // Message d'erreur pour les échecs de connexion
  const errorMessage = ref('')

  /**
   * Fonction de connexion
   * - Valide les informations d'identification via le store.
   * - Redirige l'utilisateur en cas de succès ou affiche un message d'erreur.
   */
  function login () {
    const response = authStore.login(loginEmail.value, loginPassword.value)
    if (response.success) {
      // Redirection vers la page précédente ou l'accueil
      router.push(route.query.redirect || '/')
    } else {
      // Affichage du message d'erreur
      errorMessage.value = response.message
    }
  }

  /**
   * Règles de validation pour le champ email
   * - Vérifie le format standard d'une adresse email.
   */
  const validateEmail = email => {
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailPattern.test(email) || 'Veuillez entrer un email valide.'
  }

  /**
   * Règles de validation pour le champ mot de passe
   * - Le mot de passe doit contenir au moins 4 caractères.
   */
  const validatePassword = password => {
    return password.length > 3 || 'Le mot de passe doit comporter au moins 4 caractères.'
  }
</script>

<style scoped>
  /* Styles spécifiques pour centrer et styliser le formulaire de connexion */
</style>

🧭 2. Ajouter les boutons dans AppHeader.vue et masquer le bouton d’ajout de Pokémon

vue
<template>
  <!--
  Barre d'application plate
    * flat supprime l'ombre sous la barre
  -->
  <v-app-bar flat>
    <!--
    Conteneur de la barre d'application
      * class="d-flex align-start align-center" aligne les éléments de manière flexible, alignés en haut et centrés verticalement
    -->
    <v-container class="d-flex align-start align-center">
      <!--
      Logo de l'application cliquable
        * class="mr-4 pa-0 cursor-pointer" ajoute une marge à droite, retire le padding, et change le curseur pour indiquer la cliquabilité
        * image définit le chemin vers le logo (Poké Ball) de l'application
        * size="64" définit la taille de l'avatar
        * @click redirige vers la page d'accueil
      -->
      <v-avatar
              class="mr-4 pa-0 cursor-pointer"
              image="@/assets/pokeball.svg"
              size="64"
              @click="$router.push('/')"
      />

      <!-- Titre de l'application affiché dans la barre -->
      <v-toolbar-title>Pokedex</v-toolbar-title>

      <!--
      Liens de navigation générés dynamiquement
        * v-for parcourt chaque élément dans menuItems pour créer un lien de navigation
        * :key utilise link.title pour définir une clé unique par lien
        * :icon affiche l'icône spécifiée pour chaque lien
        * :to utilise le chemin vers la route spécifiée pour chaque lien
      -->
      <v-btn
              v-for="link in menuItems"
              :key="link.title"
              :icon="link.icon"
              :to="link.path"
      />

      <!--
      Bouton de déconnexion
        * v-if="authStore.isAuthenticated" affiche le bouton si l'utilisateur est connecté
        * icon="mdi-logout" affiche l'icône de déconnexion
        * @click déclenche la fonction de déconnexion (logout)
      -->
      <v-btn
              v-if="authStore.isAuthenticated"
              icon="mdi-logout"
              @click="logout"
      />

      <!--
      Bouton de connexion (affiché si l'utilisateur n'est pas connecté)
        * v-else affiche ce bouton seulement si authStore.isAuthenticated n'existe pas
        * icon="mdi-login" affiche l'icône de connexion
        * @click redirige vers la page de connexion
      -->
      <v-btn
              v-else
              icon="mdi-login"
              @click="$router.push('/login')"
      />
    </v-container>
  </v-app-bar>

  <!--
  Notification de déconnexion réussie
    * v-model="snackbar" contrôle la visibilité du snackbar
    * color="success" applique une couleur de succès (verte) au snackbar
  -->
  <v-snackbar
          v-model="snackbar"
          color="success"
  >
    Déconnexion réussie !
  </v-snackbar>
</template>

<script setup>
  import router from '@/router'
  import { useAuthStore } from '@/stores/authStore'
  import { ref } from 'vue'

  // Utilisation du authStore pour gérer l'état de connexion de l'utilisateur
  const authStore = useAuthStore()

  /*
Définition des éléments de menu pour la navigation
  - Chaque élément contient :
    * title : le titre du lien
    * path : le chemin de la route
    * icon : l'icône du lien
*/
  const menuItems = [
    { title: 'Accueil', path: '/', icon: 'mdi-pokeball' },
    { title: 'Favoris', path: '/favoris', icon: 'mdi-heart' },
    { title: 'FAQ', path: '/faq', icon: 'mdi-frequently-asked-questions' },
    { title: 'Kanto', path: '/kantomap', icon: 'mdi-map' },
  ]

  // État pour contrôler l'affichage du snackbar de déconnexion
  const snackbar = ref(false)

  /*
Fonction de déconnexion
- Affiche le snackbar de déconnexion
- Déconnecte l'utilisateur en appelant la méthode logout() du authStore
- Redirige l'utilisateur vers la page d'accueil après la déconnexion
*/
  function logout () {
    snackbar.value = true // Afficher la notification de déconnexion
    authStore.logout() // Appeler la méthode de déconnexion du authStore
    router.push('/') // Rediriger l'utilisateur vers la page d'accueil
  }
</script>

Masquer le bouton d’ajout de Pokémon sur la page d’accueil

Ne pas oublier d'importer le store d'authentification dans votre page Index.vue

js
const authStore = useAuthStore()

Ensuite, vous pouvez utiliser le store d'authentification pour afficher ou masquer le bouton d'ajout de Pokémon en fonction de l'état de connexion de l'utilisateur.

vue
<h1 class="mb-6 text-center">
  Pokédex
  <!--
  bouton pour ajouter un pokémon
    * aria-label permet d'ajouter une description pour les lecteurs d'écran utilisés par les personnes malvoyantes
    * v-tooltip permet d'afficher une info-bulle au survol du bouton
    * @click permet de naviguer vers la page d'ajout de pokémon
    * v-if="authStore.isAuthenticated" permet de masquer le bouton si l'utilisateur n'est pas connecté
  -->
  <v-btn
          v-if="authStore.isAuthenticated"
          v-tooltip.bottom="'Ajouter un Pokémon'"
          aria-label="Ajouter un Pokémon"
          class="ml-4"
          color="primary"
          icon="mdi-plus"
          @click="$router.push('pokemons/create')"
  />
</h1>

🛡️ 3. Ajouter la protection des routes

a) Ajouter l’import du store en haut de router/index.js

js
import { useAuthStore } from '@/stores/authStore'

b) Définir les routes protégées

js
const protectedRoutes = [
  '/pokemons/create',
  // Ajouter d’autres routes protégées ici
]

c) Ajouter un "guard" global avant router.onError

js
router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()
  if (protectedRoutes.includes(to.path) && !authStore.isAuthenticated) {
    next({ path: '/login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

✅ Résultat attendu

  • L'utilisateur non connecté est redirigé vers /login s’il tente d’accéder à une route protégée
  • Après connexion, il est renvoyé à la page d’origine
  • Connexion possible uniquement avec :
    • Email : sacha@pokemon.com
    • Mot de passe : pika
  • Les boutons du menu s’adaptent dynamiquement
  • Le bouton d’ajout de Pokémon est masqué si l’utilisateur n’est pas connecté

🧪 Tests à effectuer

  1. Aller sur /pokemons/create sans être connecté → redirection automatique vers /login
  2. Tester des identifiants incorrects → affichage du message d’erreur
  3. Se connecter avec les bons identifiants → redirection vers la page protégée
  4. Cliquer sur Déconnexion → retour à l’état non connecté
  5. Vérifier que les boutons dans le menu changent correctement
  6. Vérifier que le bouton d'ajout de la page d'accueil est masqué si non connecté

📘 Documentation utile