É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ément | Statut | Action à faire |
---|---|---|
authStore.js | Déjà fourni ✅ | À importer et utiliser |
router/index.js | Basique 🟡 | À 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
Menu de navigation
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
- Email :
- 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
- Aller sur
/pokemons/create
sans être connecté → redirection automatique vers/login
- Tester des identifiants incorrects → affichage du message d’erreur
- Se connecter avec les bons identifiants → redirection vers la page protégée
- Cliquer sur Déconnexion → retour à l’état non connecté
- Vérifier que les boutons dans le menu changent correctement
- Vérifier que le bouton d'ajout de la page d'accueil est masqué si non connecté