Étape 19 — Authentification et protection de routes
Séquence démo — pas requis dans le projet personnel
L'authentification et les guards de navigation sont présentés en démonstration. Ces concepts seront abordés à l'oral mais ne font pas partie des livrables du projet individuel.
Objectifs
- Créer un store d'authentification factice (
authStore) - Construire une page de connexion avec validation
- Protéger des routes avec un guard de navigation (
router.beforeEach) - Adapter l'en-tête selon l'état de connexion (
v-if)
Prérequis — Point de départ
Cette étape nécessite d'avoir terminé les étapes 1 à 18 (séquences 1-6). Si votre code n'est pas à jour, vous pouvez repartir de la branche etape-19-start :
# Ajouter le dépôt du prof (une seule fois)
git remote add prof https://github.com/fallinov/esig-141-pokedex-vuetify.git
# Récupérer les branches et basculer
git fetch prof
git checkout -b etape-19-start prof/etape-19-start
npm installRésultat attendu

Concepts clés
Guard de navigation
Un guard de navigation, c'est comme un videur devant une boîte de nuit : il vérifie que vous avez le droit d'entrer avant de vous laisser passer. Si vous n'êtes pas sur la liste (pas authentifié), il vous renvoie ailleurs (page de connexion).
// Avant chaque changement de page...
router.beforeEach((to) => {
const authStore = useAuthStore()
// Si la route est protégée ET l'utilisateur n'est pas connecté
if (protectedRoutes.includes(to.path) && !authStore.isAuthenticated) {
return { path: '/login' } // Redirection vers la connexion
}
})Authentification factice
En production, l'authentification passe par une vraie API (JWT, OAuth, etc.). Ici, on simule avec des credentials en dur pour se concentrer sur le mécanisme : store, token, guards.
Tâches
1. Créer le store src/stores/authStore.js
Le store gère trois choses :
login(email, password): vérifie les credentials et stocke le tokenlogout(): supprime le token et l'utilisateurisAuthenticated: getter qui retournetruesi un token existe
Credentials de test : sacha@pokemon.com / pika
2. Créer la page src/pages/login.vue
La page de connexion contient :
- Un champ email avec validation (format email)
- Un champ mot de passe avec bouton pour afficher/masquer
- Un message d'erreur si les credentials sont incorrects
- Une indication des credentials de test
| Composant | Rôle |
|---|---|
v-text-field type email | Champ email avec icône mdi-email |
v-text-field type password | Champ mot de passe avec toggle visibilité |
v-alert type error | Message d'erreur conditionnel |
v-btn block | Bouton de connexion pleine largeur |
Toggle du mot de passe
Le type du champ bascule entre password et text grâce à une variable réactive showPassword :
<v-text-field
:type="showPassword ? 'text' : 'password'"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="showPassword = !showPassword"
/>3. Ajouter le guard dans src/router/index.js
Le guard intercepte chaque navigation et vérifie si la route de destination nécessite une authentification.
// Liste des routes protégées
const protectedRoutes = ['/ajouter']
// Guard global
router.beforeEach((to) => {
const authStore = useAuthStore()
if (protectedRoutes.includes(to.path) && !authStore.isAuthenticated) {
return { path: '/login' }
}
})4. Modifier App.vue — Charger le token au démarrage
Dans src/App.vue, importez le authStore et appelez authStore.loadToken() dans onMounted. Cela permet de restaurer la session utilisateur après un rechargement de page :
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/authStore'
import { usePokemonStore } from '@/stores/pokemonStore'
onMounted(async () => {
const authStore = useAuthStore()
authStore.loadToken()
const pokemonStore = usePokemonStore()
await pokemonStore.init()
})Sans cet appel, l'utilisateur serait déconnecté à chaque rechargement de page, même si le token est encore dans le localStorage.
5. Adapter l'en-tête (AppHeader)
L'en-tête affiche des éléments différents selon l'état de connexion :
- Non connecté : bouton icône « Connexion » (
mdi-login) qui mène à/login - Connecté : bouton « Ajouter » (
mdi-plus-circle) + bouton « Déconnexion » (mdi-logout) - Un snackbar confirme la déconnexion
Pour le desktop (dans la v-app-bar) :
<!-- Lien Ajouter (visible uniquement si connecté) -->
<v-btn
v-if="authStore.isAuthenticated"
icon="mdi-plus-circle"
to="/ajouter"
class="d-none d-md-flex"
/>
<!-- Bouton Déconnexion (si connecté) -->
<v-btn
v-if="authStore.isAuthenticated"
icon="mdi-logout"
class="d-none d-md-flex"
@click="handleLogout"
/>
<!-- Bouton Connexion (si non connecté) -->
<v-btn
v-else
icon="mdi-login"
to="/login"
class="d-none d-md-flex"
/>Pour le mobile (dans le v-navigation-drawer), ajoutez les mêmes éléments sous forme de v-list-item.
La fonction handleLogout déconnecte l'utilisateur, affiche un snackbar de confirmation et redirige vers l'accueil :
function handleLogout () {
authStore.logout()
snackbar.value = true
router.push('/')
}Tests
- La page
/loginaffiche un formulaire email + mot de passe sacha@pokemon.com/pika→ connexion réussie, redirection vers/- Mauvais credentials → message d'erreur affiché
- Accès à
/ajoutersans être connecté → redirection vers/login - Après connexion, l'en-tête affiche le nom et le bouton « Déconnexion »
- Après déconnexion, le bouton « Connexion » réapparaît
Solution
src/stores/authStore.js
import { defineStore } from 'pinia'
import { setAuthToken } from '@/plugins/axios'
/**
* Données factices pour simuler l'authentification
* En production, ces données viendraient d'une API
*/
const utilisateurFactice = {
email: 'sacha@pokemon.com',
name: 'Sacha Ketchum',
}
const passwordFactice = 'pika'
const tokenFactice = '0b042934e5df02c9786efb364d946e64'
export const useAuthStore = defineStore('auth', {
/**
* État initial
* - user : informations de l'utilisateur connecté (null si déconnecté)
* - token : jeton d'authentification (null si déconnecté)
*/
state: () => ({
user: null,
token: null,
}),
actions: {
/**
* Simule la connexion d'un utilisateur
* Compare les credentials avec les données factices
*/
login (email, password) {
if (email === utilisateurFactice.email && password === passwordFactice) {
// Credentials corrects → stocker l'utilisateur et le token
this.user = utilisateurFactice
this.token = tokenFactice
setAuthToken(this.token)
localStorage.setItem('token', this.token)
return { success: true, message: 'Connexion réussie' }
} else {
// Credentials incorrects → tout réinitialiser
this.user = null
this.token = null
setAuthToken(null)
localStorage.removeItem('token')
return { success: false, message: 'Mauvais email ou mot de passe !' }
}
},
/**
* Déconnecte l'utilisateur
* Supprime le token du store et du localStorage
*/
logout () {
this.user = null
this.token = null
setAuthToken(null)
localStorage.removeItem('token')
return { success: true, message: 'Déconnexion réussie' }
},
/**
* Recharge le token depuis le localStorage
* Permet de maintenir la session après un rechargement de page
*/
loadToken () {
const token = localStorage.getItem('token')
if (token) {
this.user = utilisateurFactice
this.token = token
setAuthToken(token)
}
},
},
getters: {
/**
* Retourne true si un token est présent (utilisateur connecté)
*/
isAuthenticated: state => !!state.token,
},
})src/pages/login.vue
<template>
<v-container>
<h1 class="text-h3 text-center my-6">
Connexion
</h1>
<!--
Formulaire de connexion
* Centré avec max-width et mx-auto
* @submit.prevent empêche le rechargement de la page
-->
<v-card
max-width="400"
class="mx-auto pa-6"
>
<v-form
ref="formRef"
@submit.prevent="submitLogin"
>
<!--
Champ email
* type="email" active la validation HTML5 du navigateur
* :rules applique les règles de validation Vue
-->
<v-text-field
v-model="email"
label="Email"
type="email"
:rules="emailRules"
prepend-inner-icon="mdi-email"
variant="outlined"
class="mb-2"
/>
<!--
Champ mot de passe
* :type bascule entre "password" (caché) et "text" (visible)
* Le bouton oeil permet de montrer/cacher le mot de passe
-->
<v-text-field
v-model="password"
label="Mot de passe"
:type="showPassword ? 'text' : 'password'"
:rules="passwordRules"
prepend-inner-icon="mdi-lock"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
variant="outlined"
class="mb-2"
@click:append-inner="showPassword = !showPassword"
/>
<!--
Message d'erreur si la connexion échoue
-->
<v-alert
v-if="errorMessage"
type="error"
variant="tonal"
class="mb-4"
>
{{ errorMessage }}
</v-alert>
<!--
Bouton de connexion
* type="submit" soumet le formulaire
* block prend toute la largeur
-->
<v-btn
type="submit"
color="primary"
block
size="large"
>
Se connecter
</v-btn>
</v-form>
<!--
Aide pour l'utilisateur (credentials de test)
-->
<v-card-text class="text-center text-caption mt-4">
<strong>Compte de test :</strong><br>
sacha@pokemon.com / pika
</v-card-text>
</v-card>
</v-container>
</template>
<script setup>
import { useAuthStore } from '@/stores/authStore'
const authStore = useAuthStore()
const router = useRouter()
const formRef = ref(null)
const email = ref('')
const password = ref('')
const showPassword = ref(false)
const errorMessage = ref('')
/**
* Règles de validation pour l'email
*/
const emailRules = [
v => !!v || 'L\'email est obligatoire',
v => /.+@.+\..+/.test(v) || 'L\'email doit être valide',
]
/**
* Règles de validation pour le mot de passe
*/
const passwordRules = [
v => !!v || 'Le mot de passe est obligatoire',
]
/**
* Soumission du formulaire de connexion
* 1. Valide le formulaire
* 2. Tente la connexion via authStore.login()
* 3. Redirige vers l'accueil si succès, affiche l'erreur sinon
*/
async function submitLogin () {
const { valid } = await formRef.value.validate()
if (!valid) return
errorMessage.value = ''
const result = authStore.login(email.value, password.value)
if (result.success) {
router.push('/')
} else {
errorMessage.value = result.message
}
}
</script>src/App.vue (ajout de loadToken)
<template>
<v-app>
<app-header />
<v-main>
<router-view />
</v-main>
<app-footer />
</v-app>
</template>
<script setup>
import AppHeader from '@/components/AppHeader.vue'
import AppFooter from '@/components/AppFooter.vue'
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/authStore'
import { usePokemonStore } from '@/stores/pokemonStore'
onMounted(async () => {
const authStore = useAuthStore()
authStore.loadToken()
const pokemonStore = usePokemonStore()
await pokemonStore.init()
})
</script>src/components/AppHeader.vue
<template>
<v-app-bar flat>
<v-container class="d-flex align-center">
<v-app-bar-nav-icon
class="d-md-none"
@click="drawer = !drawer"
/>
<v-avatar
class="mr-4 pa-0 cursor-pointer"
image="@/assets/pokeball.svg"
size="64"
@click="$router.push('/')"
/>
<v-toolbar-title>Pokédex</v-toolbar-title>
<v-btn
v-for="link in menuItems"
:key="link.title"
:icon="link.icon"
:to="link.path"
class="d-none d-md-flex"
/>
<!-- Lien Ajouter (visible uniquement si connecté) -->
<v-btn
v-if="authStore.isAuthenticated"
icon="mdi-plus-circle"
to="/ajouter"
class="d-none d-md-flex"
/>
<!-- Bouton Déconnexion (si connecté) -->
<v-btn
v-if="authStore.isAuthenticated"
icon="mdi-logout"
class="d-none d-md-flex"
@click="handleLogout"
/>
<!-- Bouton Connexion (si non connecté) -->
<v-btn
v-else
icon="mdi-login"
to="/login"
class="d-none d-md-flex"
/>
</v-container>
</v-app-bar>
<!-- Tiroir de navigation (mobile) -->
<v-navigation-drawer
v-model="drawer"
temporary
>
<v-list nav>
<v-list-item
v-for="link in menuItems"
:key="link.title"
:prepend-icon="link.icon"
:title="link.title"
:to="link.path"
@click="drawer = false"
/>
<!-- Lien Ajouter (mobile, si connecté) -->
<v-list-item
v-if="authStore.isAuthenticated"
prepend-icon="mdi-plus-circle"
title="Ajouter"
to="/ajouter"
@click="drawer = false"
/>
<v-divider class="my-2" />
<!-- Déconnexion (mobile) -->
<v-list-item
v-if="authStore.isAuthenticated"
prepend-icon="mdi-logout"
title="Déconnexion"
@click="handleLogout(); drawer = false"
/>
<!-- Connexion (mobile) -->
<v-list-item
v-else
prepend-icon="mdi-login"
title="Connexion"
to="/login"
@click="drawer = false"
/>
</v-list>
</v-navigation-drawer>
<!-- Snackbar de confirmation de déconnexion -->
<v-snackbar
v-model="snackbar"
:timeout="2000"
color="info"
>
Déconnexion réussie
</v-snackbar>
</template>
<script setup>
import { useAuthStore } from '@/stores/authStore'
const authStore = useAuthStore()
const router = useRouter()
const menuItems = [
{ title: 'Accueil', path: '/', icon: 'mdi-pokeball' },
{ title: 'Favoris', path: '/favoris', icon: 'mdi-heart' },
{ title: 'À propos', path: '/a-propos', icon: 'mdi-information' },
]
const drawer = ref(false)
const snackbar = ref(false)
function handleLogout () {
authStore.logout()
snackbar.value = true
router.push('/')
}
</script>src/router/index.js (ajout du guard)
import { createRouter, createWebHistory } from 'vue-router/auto'
import { routes } from 'vue-router/auto-routes'
import { useAuthStore } from '@/stores/authStore'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
/**
* Routes qui nécessitent d'être authentifié
* Si l'utilisateur n'est pas connecté, il est redirigé vers /login
*/
const protectedRoutes = ['/ajouter']
/**
* Guard de navigation globale
* Vérifie l'authentification avant chaque changement de route
*/
router.beforeEach((to) => {
const authStore = useAuthStore()
// Vérifier si la route nécessite une authentification
if (protectedRoutes.includes(to.path) && !authStore.isAuthenticated) {
// Rediriger vers la page de connexion
return { path: '/login' }
}
})
export default routerCommit
git add -A
git commit -m "feat: authentification factice et guards de navigation"