Tutoriel Ruby on Rails

Apprendre Rails par l'exemple

Michael Hartl

Contenu

  1. Chapitre 1 De zéro au déploiement
    1. 1.1 Introduction
      1. 1.1.1 Commentaires pour les lecteurs différents
      2. 1.1.2 “Dimensionner” Rails
      3. 1.1.3 Conventions utilisées dans ce livre
    2. 1.2 Debout et au boulot
      1. 1.2.1 Environnements de développement
        1. IDEs
        2. Éditeurs de texte et lignes de commande
        3. Navigateurs
        4. Note à propos des outils
      2. 1.2.2 Ruby, RubyGems, Rails, et Git
        1. Installation de Rails (Windows)
        2. Installer Git
        3. Installer Ruby
        4. Installer RubyGems
        5. Installer Rails
      3. 1.2.3 La première application
      4. 1.2.4 Bundler
      5. 1.2.5 Le serveur rails (rails server)
      6. 1.2.6 Modèles-Vue-Contrôleur (MVC)
    3. 1.3 Contrôle de versions avec Git
      1. 1.3.1 Installation et réglages
        1. Initialisation des réglages système
        2. Initialisation des réglages du dépôt (repository)
      2. 1.3.2 Ajout et mandat de dépôt
      3. 1.3.3 Qu'est-ce que Git peut faire de bien pour vous ?
      4. 1.3.4 GitHub
      5. 1.3.5 Branch, edit, commit, merge
        1. Branch
        2. Edit
        3. Commit
        4. Merge
        5. Push
    4. 1.4 Déploiement
      1. 1.4.1 Réglages Heroku
      2. 1.4.2 Déploiement Heroku, première étape
      3. 1.4.3 Déploiement Heroku, seconde étape
      4. 1.4.4 Commandes Heroku
    5. 1.5 Conclusion
  2. Chapitre 2 Une application démo
    1. 2.1 Planifier l'application
      1. 2.1.1 Modéliser les utilisateurs
      2. 2.1.2 Modéliser les micro-messages
    2. 2.2 La ressource Utilisateurs (Users)
      1. 2.2.1 Un tour de l'utilisateur
      2. 2.2.2 MVC en action
      3. 2.2.3 Faiblesses de la ressource Utilisateurs (Users)
    3. 2.3 La ressource Micro-messages (Microposts)
      1. 2.3.1 Un petit tour du micro-message
      2. 2.3.2 Appliquer le micro aux micro-messages
      3. 2.3.3 Un utilisateur has_many micro-messages
      4. 2.3.4 Hiérarchie des héritages
      5. 2.3.5 Déployer l'application Démo
    4. 2.4 Conclusion
  3. Chapitre 3 Pages statiques courantes
    1. 3.1 Pages statiques
      1. 3.1.1 Pages HTML statiques
      2. 3.1.2 Les pages statiques avec Rails
    2. 3.2 Premiers tests
      1. 3.2.1 Outils de test
        1. Auto-test
      2. 3.2.2 TDD : Rouge, Vert, Refactor
        1. Spork
        2. Rouge
        3. Vert
        4. Refactor
    3. 3.3 Pages (un peu) dynamiques
      1. 3.3.1 Test d'un changement de titre
      2. 3.3.2 Réussir les tests de titre
      3. 3.3.3 Variables d'instance et Ruby embarqué
      4. 3.3.4 Supprimer les répétitions avec les layouts
    4. 3.4 Conclusion
    5. 3.5 Exercices
  4. Chapitre 4 Rails au goût Ruby
    1. 4.1 Motivation
      1. 4.1.1 Un helper pour le titre
      2. 4.1.2 Feuilles de styles (CSS — Cascading Style Sheets)
    2. 4.2 Chaines de caractères et méthodes
      1. 4.2.1 Commentaires
      2. 4.2.2 Chaines de caractères
        1. Impression
        2. Chaines de caractères « apostrophées »
      3. 4.2.3 Objets et passage de message
      4. 4.2.4 Définition de méthode
      5. 4.2.5 Retour à l'« helper » de titre
    3. 4.3 Autres structures de données
      1. 4.3.1 Tableaux et rangs
      2. 4.3.2 Blocs
      3. 4.3.3 Tables de hachage et symboles
      4. 4.3.4 CSS revisitées
    4. 4.4 Classes Ruby
      1. 4.4.1 Constructeurs
      2. 4.4.2 Héritages de classes
      3. 4.4.3 Modifier les classes d'origine
      4. 4.4.4 Classe de contrôleur
      5. 4.4.5 La classe utilisateur
    5. 4.5 Exercices
  5. Chapitre 5 Poursuivre la mise en page
    1. 5.1 Ajout de structure
      1. 5.1.1 Navigation du site
      2. 5.1.2 Personnalisation CSS
      3. 5.1.3 Partiels
    2. 5.2 Liens pour la mise en page
      1. 5.2.1 Test d'intégration
      2. 5.2.2 Routes Rails
      3. 5.2.3 Nommer les routes
    3. 5.3 Inscription de l'utilisateur : une première étape
      1. 5.3.1 Contrôleur Utilisateur
      2. 5.3.2 URL d'inscription
    4. 5.4 Conclusion
    5. 5.5 Exercices
  6. Chapitre 6 Modéliser et afficher les utilisateurs, partie I
    1. 6.1 Modèle utilisateur
      1. 6.1.1 Migrations de la base de données
      2. 6.1.2 Le fichier modèle
        1. Annotation des modèles
        2. Attributs accessibles
      3. 6.1.3 Créer des objets Utilisateur
      4. 6.1.4 Recherche dans les objets Utilisateurs
      5. 6.1.5 Actualisation des objets Utilisateurs
    2. 6.2 Validations utilisateur
      1. 6.2.1 Valider l'existence
      2. 6.2.2 Valider la longueur
      3. 6.2.3 Valider le format
      4. 6.2.4 Valider l'unicité
        1. L'avertissement d'unicité
    3. 6.3 Afficher les utilisateurs
      1. 6.3.1 Débuggage et environnements Rails
      2. 6.3.2 Modèle, Vue et Contrôleur Utilisateur
      3. 6.3.3 Ressource Utilisateurs
        1. Paramètres pour le déboggage
    4. 6.4 Conclusion
    5. 6.5 Exercices
  7. Chapitre 7 Modéliser et afficher les utilisateurs, partie II
    1. 7.1 Mots de passe non sécurisés
      1. 7.1.1 Valider le mot de passe
      2. 7.1.2 Migrer un mot de passe
      3. 7.1.3 Fonction de rappel dans l'Active Record
    2. 7.2 Sécuriser les mots de passe
      1. 7.2.1 Test de mot de passe sécurisé
      2. 7.2.2 Un peu de théorie sur la sécurisation des mots de passe
      3. 7.2.3 Implémenter la méthode has_password?
      4. 7.2.4 Méthode d'authentification
    3. 7.3 Meilleures vues d'utilisateurs
      1. 7.3.1 Tester la page de l'utilisateur (avec factories)
      2. 7.3.2 Un nom et un Gravatar
        1. Un « helper » de Gravatar
      3. 7.3.3 Une barre utilisateur latérale
    4. 7.4 Conclusion
      1. 7.4.1 Dépôt Git
      2. 7.4.2 Déploiement Heroku
    5. 7.5 Exercices
  8. Chapitre 8 Inscription
    1. 8.1 Formulaire d'inscription
      1. 8.1.1 Utiliser form_for
      2. 8.1.2 Le formulaire HTML
    2. 8.2 Échec de l'inscription
      1. 8.2.1 Test de l'échec
      2. 8.2.2 Un formulaire fonctionnel
      3. 8.2.3 Inscription : messages d'erreur
      4. 8.2.4 Filtrer les paramètres d'identification
    3. 8.3 Succès de l'inscription
      1. 8.3.1 Tester le succès de l'inscription
      2. 8.3.2 Le formulaire d'inscription finalisé
      3. 8.3.3 Le message « flash »
      4. 8.3.4 La première inscription
    4. 8.4 Test d'intégration RSpec
      1. 8.4.1 Tests d'intégration avec les styles
      2. 8.4.2 Un échec d'inscription ne devrait pas créer un nouvel utilisateur
      3. 8.4.3 Le succès d'une inscription devrait créer un nouvel utilisateur
    5. 8.5 Conclusion
    6. 8.6 Exercices
  9. Chapitre 9 Connexion, déconnexion
    1. 9.1 Les sessions
      1. 9.1.1 Le contrôleur de session
      2. 9.1.2 Formulaire d'identification
    2. 9.2 Échec de l'identification
      1. 9.2.1 Examen de la soumission du formulaire
      2. 9.2.2 Échec de l'identification (test et code)
    3. 9.3 Succès de l'identification
      1. 9.3.1 L'action create finalisée
      2. 9.3.2 Se souvenir de moi
      3. 9.3.3 Utilisateur courant
    4. 9.4 Déconnexion
      1. 9.4.1 Détruire la session
      2. 9.4.2 Connexion à l'inscription
      3. 9.4.3 Changement des liens de la mise en page
      4. 9.4.4 Test d'intégration de l'identification/déconnexion
    5. 9.5 Conclusion
    6. 9.6 Exercices
  10. Chapitre 10 Actualiser, afficher et supprimer des utilisateurs
    1. 10.1 Actualiser l'utilisateur
      1. 10.1.1 Formulaire de modification
      2. 10.1.2 Permettre les modifications
    2. 10.2 Protéger les pages
      1. 10.2.1 Utilisateurs identifiés requis
      2. 10.2.2 Nécessité du bon utilisateur
      3. 10.2.3 Redirection conviviale
    3. 10.3 Afficher les utilisateurs
      1. 10.3.1 Liste des utilisateurs
      2. 10.3.2 Exemples d'utilisateurs
      3. 10.3.3 Pagination
        1. Test de la pagination
      4. 10.3.4 Restructuration des partiels
    4. 10.4 Supprimer des utilisateurs
      1. 10.4.1 Utilisateurs administrateurs
        1. Révision de attr_accessible
      2. 10.4.2 L'action destroy (« supprimer »)
    5. 10.5 Conclusion
    6. 10.6 Exercices
  11. Chapitre 11 Micro-messages d'utilisateurs
    1. 11.1 Le modèle Micropost (« Micro-message »)
      1. 11.1.1 Le modèle initial
        1. Attributs accessibles
      2. 11.1.2 Associations Utilisateur/micro-messages
      3. 11.1.3 Affinements du micro-message
        1. Portée par défaut
        2. Dépendances de la suppression
      4. 11.1.4 Validations du micro-message
    2. 11.2 Afficher les micro-messages
      1. 11.2.1 Etoffement de la page de l'utilisateur
      2. 11.2.2 Exemples de micro-messages
    3. 11.3 Manipuler les micro-messages
      1. 11.3.1 Contrôle de l'accès
      2. 11.3.2 Créer des micro-messages
      3. 11.3.3 Une proto-alimentation
      4. 11.3.4 Supprimer des micro-messages
      5. 11.3.5 Test de la nouvelle page d'accueil
    4. 11.4 Conclusion
    5. 11.5 Exercices
  12. Chapitre 12 Suivi des utilisateurs
    1. 12.1 Le modèle Relation (Relationship model)
      1. 12.1.1 Un problème du modèle de données (et sa solution)
      2. 12.1.2 Associations Utilisateur/Relations
      3. 12.1.3 Validations
      4. 12.1.4 Auteurs suivis
      5. 12.1.5 Les Lecteurs
    2. 12.2 Une interface web pour les auteurs et les lecteurs
      1. 12.2.1 Exemple de donnée de suivi
      2. 12.2.2 Statistiques et formulaire de suivi
      3. 12.2.3 Pages d'auteurs suivis et de lecteurs
      4. 12.2.4 Un bouton de suivi standard
      5. 12.2.5 Un bouton fonctionnant avec Ajax
    3. 12.3 L'état de l'alimentation
      1. 12.3.1 Motivation et stratégie
      2. 12.3.2 Une première implémentation de peuplement
      3. 12.3.3 Champs d'application, sous-sélections et lambda
      4. 12.3.4 Nouvel état de l'alimentation
    4. 12.4 Conclusion
      1. 12.4.1 Extensions de l'application exemple
        1. Réponses
        2. Notification
        3. Notifications aux lecteurs
        4. Rappel du mot de passe
        5. Confirmation d'inscription
        6. Alimentation RSS
        7. REST API
        8. Recherche
      2. 12.4.2 Guide vers d'autres ressources
    5. 12.5 Exercices

Avant-propos

Ma précédente compagnie (CD Baby) fut une des premières à basculer intégralement vers Ruby on Rails, et à rebasculer aussi intégralement vers PHP (googlez-moi si vous voulez prendre la mesure du drame). On m'a tellement recommandé ce livre Michael Hartl que je n'ai pu faire autrement que de le lire. C'est ainsi que le Tutoriel Ruby on Rails m'a fait revenir à nouveau à Rails.

Bien qu'ayant parcouru de nombreux livres sur Rails, c'est ce tutoriel-là qui m'a véritablement « mis en possession » de Rails. Tout est fait ici « à la manière de Rails » — une manière qui ne m'avait jamais semblé naturelle avant que je ne lise ce livre. C'est aussi le seul ouvrage sur Rails qui met en place, d'un bout à l'autre, un Développement Dirigé par les Tests (Test-Driven Development), une approche que je savais hautement recommandée par les experts mais dont je n'avais jamais compris aussi bien la pertinence que dans ce livre. Enfin, en incluant Git, GitHub et Heroku dans les exemples de la démonstration, l'auteur vous donne vraiment le goût de ce qu'est le développement d'un projet dans la vie réelle. Et le exemples de code ne sont pas en reste.

La narration linéaire adoptée par ce tutoriel est vraiment un bon format. Personnellement, j'ai étudié Le Tutoriel Rails en trois longues journées, en faisant tous les exemples et les exercices proposés à la fin de chaque chapitre. C'est en lisant ce livre du début à la fin, sans sauter la moindre partie, qu'on en tire tout le bénéfice.

Régalez-vous !

Derek Sivers (sivers.org)
Précédemment : Fondateur de CD Baby
Actuellement : Fondateur de Thoughts Ltd.

Remerciements

Ce Tutoriel Ruby on Rails doit beaucoup à mon livre précédent sur Rails, RailsSpace, et donc à mon co-auteur Aurelius Prochazka. J'aimerais remercier Aure à la fois pour le travail qu'il a accompli sur ce précédent livre et pour son soutien pour le présent ouvrage. J'aimerais aussi remercier Debra Williams Cauley, mon éditeur pour les deux ouvrages ; aussi longtemps qu'elle jouera avec moi au baseball, je continuerai d'écrire des livres pour elle.

J'aimerais remercier une longue liste de Rubyistes qui m'ont parlé et inspiré au cours des années : David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, les gens bien de Pivotal Labs, le gang Heroku, les mecs de thoughtbot et l'équipe de GitHub. Enfin, tellement, tellement, tellement de lecteurs — beaucoup trop pour les citer tous — qui ont contribué par leur rapport de bogues et leurs suggestions durant l'écriture de ce livre, et je tiens à saluer leur aide sans laquelle ce livre ne serait pas ce qu'il est.

À propos de l'auteur

Michael Hartl est programmeur, éducateur et entrepreneur. Il est le co-auteur de RailsSpace, un tutoriel Rails publié en 2007, et a été co-fondateur et développeur en chef de Insoshi, une plateforme de réseau social populaire en Ruby on Rails. Précédement, il a enseigné la théorie et la physique informatique au California Institute of Technology (Caltech), où il a reçu le Lifetime Achievement Award for Excellence en enseignement. Michael est diplômé du Harvard College, a un Ph.D. en physique (un doctorat. NdT) de Caltech, et il est ancien élève du programme des entrepreneurs Y Combinator.

Copyright et license

Le Tutoriel Ruby on Rails : apprendre Rails par l'exemple. Copyright © 2010 par Michael Hartl. Tout le code source du Tutoriel Ruby on Rails est disponible sous la license MIT License et la licence Beerware License.

   Copyright (c) 2010 Michael Hartl

   Permission est accordée, à titre gratuit, à toute personne obtenant
   une copie de ce logiciel et la documentation associée, pour faire des 
   modification dans le logiciel sans restriction et sans limitation des
   droits d’utiliser, copier, modifier, fusionner, publier, distribuer,
   concéder sous licence, et / ou de vendre les copies du Logiciel, et à
   autoriser les personnes auxquelles le Logiciel est meublé de le faire, 
   sous réserve des conditions suivantes:

   L’avis de copyright ci-dessus et cette autorisation doit être inclus
   dans toutes les copies ou parties substantielles du Logiciel.

   LE LOGICIEL EST FOURNI «TEL QUEL», SANS GARANTIE D’AUCUNE SORTE, 
   EXPLICITE OU IMPLICITE, Y COMPRIS, MAIS SANS S’Y LIMITER, LES 
   GARANTIES DE QUALITÉ MARCHANDE, ADAPTATION À UN USAGE PARTICULIER ET
   D’ABSENCE DE CONTREFAÇON. EN AUCUN CAS LES AUTEURS OU TITULAIRES DU
   ETRE TENU RESPONSABLE DE TOUT DOMMAGE, RÉCLAMATION OU AUTRES
   RESPONSABILITÉ, SOIT DANS UNE ACTION DE CONTRAT, UN TORT OU AUTRE,
   PROVENANT DE, DE OU EN RELATION AVEC LE LOGICIEL OU L’UTILISATION OU
   DE TRANSACTIONS AUTRES LE LOGICIEL.
	
/*
 * ------------------------------------------------------------
 * "LA LICENCE BEERWARE" (Révision 42) :
 * Michael Hartl a écrit ce code. Aussi longtemps que vous
 * conservez cette note, vous pouvez faire ce que vous voulez
 * de ce travail. Si nous nous rencontrons un jour, et que vous
 * pensez que ce travail en vaut la peine, vous pourrez me
 * payer une bière en retour.
 * ------------------------------------------------------------
 */

Chapitre 9 Connexion, déconnexion

Maintenant que les nouveaux utilisateurs peuvent s'inscrire sur notre site (chapitre 8), il est temps de donner à ces utilisateurs enregistrés la possibilité de se connecter (de s'identifier) et se déconnecter. Cela nous permettra d'ajouter des personnalisations du layout basées sur l'état de l'internaute visitant le site, utilisateur identifié ou simple visiteur. Par exemple, dans ce chapitre nous actualiserons l'entête avec les liens d'identification et de déconnexion et le lien pour rejoindre le profil ; au chapitre 11, nous utiliserons l'identité d'un utilisateur connecté pour créer des micro-messages associés à cet utilisateur, et au chapitre 12 nous permettrons à l'utilisateur de suivre les micro-messages d'autres utilisateurs de l'application.

Avoir des utilisateurs connectés nous permettra aussi d'implémenter un modèle de sécurité restreignant l'accès de certaines pages à des utilisateurs particuliers. Par exemple, comme nous le verrons au chapitre 10, seuls les visiteurs connectés seront en mesure d'accéder à la page de modification des informations utilisateur. Le système d'identification (de connexion) rendra aussi possible certains privilèges pour les utilisateurs administrateurs, comme la possibilité (chapitre 10) de détruire des utilisateurs de la base de données.

Comme dans les chapitres précédents, nous ferons notre travail sur une branche sujet et fusionnerons les changements à la fin :

$ git checkout -b sign-in-out

9.1 Les sessions

Une session est une connexion semi-permanente entre deux ordinateurs, tels qu'un ordinateur client jouant un navigateur web et un serveur jouant Rails. Il existe plusieurs modèles de comportement de session sur le web : « oublier » la session à la fermeture du navigateur, utiliser une option « se souvenir de moi » pour des sessions persistantes, et se souvenir des sessions jusqu'à ce que l'utilisateur se déconnecte explicitement.1 Nous opterons pour la dernière de ces options : quand un utilisateur s'identifie, nous nous souviendrons de son statut « pour toujours »,2 n'effaçant sa session que lorsqu'il se déconnectera explicitement de lui-même.

Il est pratique de modeler les sessions comme une ressource REST : nous aurons une page d'identification pour les nouvelles sessions (new), l'identification créera une session (create), et la déconnexion la détruira (destroy). Nous aurons par conséquent besoin d'un contrôleur Sessions avec les actions new (nouvelle), create (créer) et destroy (détruire). Contrairement au cas du contrôleur Users, qui utilise une base de données (via le modèle User) pour les données persistantes, le contrôleur Sessions utilisera un cookie, qui est un petit morceau de texte placé sur le navigateur de l'utilisateur. Le plus gros du travail de l'identification consiste à construire cette machinerie d'authentification basée sur les cookies. Dans cette section et la suivante, nous allons nous préparer à ce travail en construisant un contrôleur Sessions, un formulaire d'identification et les actions de contrôleur correspondantes (le plus gros ce travail est similaire à l'inscription de l'utilisateur du chapitre 8). Nous terminerons alors l'identification de l'utilisateur avec le code nécessaire manipulant les cookies à la section 9.3.

9.1.1 Contrôleur Sessions

Les éléments de l'inscription et de l'identification correspondent aux actions REST particulières du contrôleur Sessions : le formulaire d'identification est traité par l'action new (couverte par cette section), et plus précisément l'identification est traitée en envoyant une requête POST à l'action create (section 9.2 et section 9.3), et la déconnexion est traitée en envoyant une requête DELETE à l'action destroy (section 9.4) (rappelez-vous de l'association des verbes HTTP avec les actions REST de la table 6.2.) Puisque nous savons que nous avons besoin d'une action new, nous pouvons la créer en générant le contrôleur Session (comme pour le contrôleur Users de l'extrait 5.23):3

$ rails generate controller Sessions new
$ rm -rf spec/views
$ rm -rf spec/helpers

Maintenant, comme avec le formulaire d'inscription à la section 8.1, nous créons un nouveau fichier pour le spec du contrôleur Sessions et ajoutons une paire de tests pour l'action new et la vue correspondante (extrait 9.1) (ce modèle devrait commencer à vous être familier maintenant).

Extrait 9.1. Tests pour l'action new et la vue de la session.
spec/controllers/sessions_controller_spec.rb
require 'spec_helper'

describe SessionsController do
  render_views

  describe "GET 'new'" do

    it "devrait réussir" do
      get :new
      response.should be_success
    end

    it "devrait avoir le bon titre" do
      get :new
      response.should have_selector("titre", :content => "S'identifier")
    end
  end
end

Pour faire réussir ces tests, nous avons d'abord besoin d'ajouter une route pour l'action new, et tant que nous y serons, nous créerons toutes les actions nécessaire au long de ce chapitre. Nous suivons de façon générale l'exemple de l'extrait 6.26, mais dans ce cas nous définissons seulement les actions particulières dont nous avons besoin, c'est-à-dire new, create et destroy, et ajoutons aussi les routes nommées pour l'identification et la déconnexion (extrait 9.2).

Extrait 9.2. Ajout d'une ressource pour obtenir les actions RESTful pour les sessions.
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users
  resources :sessions, :only => [:new, :create, :destroy]

  match '/signup',  :to => 'users#new'
  match '/signin',  :to => 'sessions#new'
  match '/signout', :to => 'sessions#destroy'
  .
  .
  .
end

Comme vous pouvez le voir, les méthodes resources (ressources) peuvent prendre une table d'options, qui dans ce cas possède une clé :only et une valeur égale à un tableau des actions à laquelle doit répondre le contrôleur Sessions. Les ressources définies dans l'extrait 9.2 fournissent des URLs et des actions similaires à celles des utilisateurs (Table 6.2), comme montré dans la table 9.1.

Requête HTTPURLRoute nomméeActionBut
GET/signinsignin_pathnewpage pour une nouvelle session (identification)
POST/sessionssessions_pathcreatecrée une nouvelle session
DELETE/signoutsignout_pathdestroyefface la session (déconnexion)
Table 9.1: Routes RESTful fournies par les règles de sessions de l'extrait 9.2.

Nous pouvons obtenir la réussite du second test de l'extrait 9.1 en ajoutant la variable d'instance titre adéquate à l'action new, comme dans l'extrait 9.3 (qui définit aussi les actions create et destroy pour référence future).

Extrait 9.3. Ajout du titre pour la page d'identification.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
    @titre = "S'identifier"
  end

  def create
  end

  def destroy
  end
end

Avec ça, les tests de l'extrait 9.1 devraient réussir, et nous sommes prêts à construire le formulaire d'identification.

9.1.2 Formulaire d'identification

Le formulaire d'identification (ou, de façon équivalente, le formulaire de nouvelle session) est similaire en apparence au formulaire d'inscription, à l'exception prêt que nous n'avons que deux champs (email et mot de passe) au lieu de quatre. Une maquette est présentée dans l'illustration 9.1.

signin_mockup
Illustration 9.1: Une maquette du formulaire d'identification. (taille normale)

Rappelez-vous, de l'extrait 8.2, que le formulaire d'inscription utilise l'helper form_for, prenant en argument la variable d'instance utilisateur @user :

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

La différence principale avec le formulaire de nouvelle session est que nous n'avons pas de modèle Session, et ainsi pas de variable @session analogue à la variable @user. Cela signifie que, en construisant le formulaire de nouvelle session, nous devons donner à form_for légèrement plus d'informations . En particulier, alors que :

form_for(@user)

… permet à Rails de déduire que action du formulaire devrait être le POST de l'URL /users, dans le cas des sessions nous avons besoin d'indiquer le nom de la ressource tout comme l'URL appropriée :

form_for(:session, :url => sessions_path)

Puisque nous authentifions les utilisateurs avec les adresses mail et les mots de passe, nous avons besoin d'un champ pour chacun à l'intérieur du formulaire ; le résultat apparait dans l'extrait 9.4.

Extrait 9.4. Code pour le formulaire d'identification.
app/views/sessions/new.html.erb
<h1>Identification</h1>

<%= form_for(:session, :url => sessions_path) do |f| %>
  <div class="field">
    <%= f.label :email, "eMail" %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password, "Mot de passe" %><br />
    <%= f.password_field :password %>
  </div>
  <div class="actions">
    <%= f.submit "S'identifier" %>
  </div>
<% end %>

<p>Pas encore inscrit ? <%= link_to "S'inscrire !", signup_path %></p>

Avec le code de l'extrait 9.4, le formulaire d'identification apparait comme dans l'illustration 9.2.

signin_form
Illustration 9.2: Le formulaire d'identification (/sessions/new). (taille normale)

Bien que nous allions bientôt perdre l'habitude de regarder le code HTML généré par Rails (et faire plutôt confiance aux helpers pour faire leur travail), pour le moment jetons-y un coup d'œil (extrait 9.5).

Extrait 9.5. HTML pour le formulaire d'identification produit par l'extrait 9.4.
<form action="/sessions" method="post">
  <div class="field">
    <label for="session_email">eMail</label><br />
    <input id="session_email" name="session[email]" size="30" type="text" />

  </div>
  <div class="field">
    <label for="session_password">Mot de passe</label><br />
    <input id="session_password" name="session[password]" size="30"
           type="password" />
  </div>
  <div class="actions">
    <input id="session_submit" name="commit" type="submit" value="S'identifier" />
  </div>
</form>

En comparant l'extrait 9.5 avec l'extrait 8.5, vous devriez pouvoir deviner que soumettre ce formulaire produira une table paramsparams[:session][:email] et params[:session][:password] correspondent au champs email et mot de passe. Traiter cette soumission — et, en particulier, authentifier les utilisateurs en se basant sur l'email et le mot de passe soumis — est le projet des deux prochaines sections.

9.2 Échec de l'identification

Comme dans le cas de la création d'utilisateurs (signup), la première étape dans la création de sessions (signin) est de traiter les entrées invalides. Nous allons commencer par revoir ce qui se passe quand un formulaire est soumis, et nous arranger alors pour afficher un message d'erreur utile dans le cas de l'échec de l'identification (comme présenté dans la maquette de l'illustration 9.3). Enfin, nous poserons les bases d'une identification réussie (section 9.3) en évaluant chaque soumission de l'identification en se basant sur la validité de sa combinaison email/mot de passe.

signin_failure_mockup
Illustration 9.3: Une maquette de l'échec de l'identification. (taille normale)

9.2.1 Examen de la soumission du formulaire

Commençons par définir une action create minimale pour le contrôleur Sessions (extrait 9.6), qui ne fait rien d'autre que rendre la vue new. Soumettre le formulaire /sessions/new avec des champs vierges puis renvoyer le résultat vu dans l'illustration 9.4.

Extrait 9.6. Une version préliminaire de l'action create de Sessions.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    render 'new'
  end
  .
  .
  .
end
initial_failed_signin_rails_3
Illustration 9.4: L'échec de l'identification initiale, avec create comme dans l'extrait 9.6(taille normale)

L'inspection attentive des informations de débuggage de l'illustration 9.4 montre que, comme soulevé à la fin de la section 9.1.2, de la soumission résulte une table params contenant l'email et le mot de passe sous la clé :session :

--- !map:ActiveSupport::HashWithIndifferentAccess
commit: S'identifier
session: !ActiveSupport::HashWithIndifferentAccess 
  password: ""
  email: ""
authenticity_token: BlO65PA1oS5vqrv591dt9B22HGSWW0HbBtoHKbBKYDQ=
action: create
controller: sessions

Comme dans le cas de l'inscription de l'utilisateur (illustration 8.6) ces paramètres forment une table imbriquée comme celle que nous avons vue dans l'extrait 4.5. En particulier, params contient une table imbriquée de la forme :

{ :session => { :password => "", :email => "" } }

Cela signifie que :

params[:session]

… est lui-même une table :

{ :password => "", :email => "" }

Comme résultat :

params[:session][:email]

… est l'adresse mail soumise et :

params[:session][:password]

… est le mot de passe soumis.

En d'autres termes, à l'intérieur de l'action create, la table params contient toutes les informations dont nous avons besoin pour authentifier les utilisateurs par l'email et le mot de passe. Ça n'est pas le fait du hasard, mais nous avons déjà développé exactement la méthode nécessaire : User.authenticate à la section 7.2.4 (extrait 7.12). En se souvenant que authenticate retourne nil pour une authentification invalide, notre stratégie pour l'identification des utilisateurs peut être résumée comme suit :

def create
  user = User.authenticate(params[:session][:email],
                           params[:session][:password])
  if user.nil?
    # Crée un message d'erreur et rend le formulaire d'identification.
  else
    # Authentifie l'utilisateur et redirige vers la page d'affichage.
  end
end

9.2.2 Échec de l'identification (test et code)

Dans le but de traiter un échec d'identification, d'abord nous avons besoin de déterminer que c'est un échec. Les tests suivent l'exemple des tests analogues pour l'inscription de l'utilisateur (extrait 8.6), comme vu dans l'extrait 9.7.

Extrait 9.7. Tests pour un échec d'identification.
spec/controllers/sessions_controller_spec.rb
require 'spec_helper'

describe SessionsController do
  render_views
  .
  .
  .
  describe "POST 'create'" do

    describe "invalid signin" do

      before(:each) do
        @attr = { :email => "email@example.com", :password => "invalid" }
      end

      it "devrait re-rendre la page new" do
        post :create, :session => @attr
        response.should render_template('new')
      end

      it "devrait avoir le bon titre" do
        post :create, :session => @attr
        response.should have_selector("title", :content => "S'identifier")
      end

      it "devait avoir un message flash.now" do
        post :create, :session => @attr
        flash.now[:error].should =~ /invalid/i
      end
    end
  end
end

Le code de l'application nécessaire pour faire réussir ces tests est présenté dans l'extrait 9.8. Comme promis à la section 9.2.1, nous extrayons l'adresse mail soumise et le mot de passe de la table params, et les passons ensuite à la méthode User.authenticate. Si l'utilisateur n'est pas authentifié (c'est-à-dire : si la méthode d'authentification renvoie nil), nous définissons le titre et rendons à nouveau le formulaire d'identification.4 Nous traiterons l'autre branche de la déclaration if-else à la section 9.3 ; pour le moment, nous allons juste laissé un commentaire descriptif.

Extrait 9.8. Code pour une tentative ratée d'identification.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.authenticate(params[:session][:email],
                             params[:session][:password])
    if user.nil?
      flash.now[:error] = "Combinaison Email/Mot de passe invalide."
      @titre = "S'identifier"
      render 'new'
    else
      # Authentifie l'utilisateur et redirige vers sa page d'affichage.
    end
  end
  .
  .
  .
end

Rappelez-vous de la section 8.4.2 : nous avons affiché les erreurs de l'inscription en utilisant les messages d'erreur du modèle User. Puisque la session n'est pas un modèle Active Record, cette stratégie ne fonctionnera pas ici, donc à la place nous déposons un message dans le flash (ou, plus exactement, dans flash.now ; voyez le Box 9.1). Grâce au message flash affiché dans le layout du site (extrait 8.16), le message flash[:error] sera automatiquement affiché ; grâce aux CSS Blueprint, il aura automatiquement une belle stylisation (illustration 9.5).

failed_signin
Illustration 9.5: Une identification ratée (avec un message flash). (taille normale)

9.3 Réussite de l'identification

Ayant traité un échec de l'identification, nous avons besoin maintenant, effectivement, de pouvoir authentifier un utilisateur. La maquette de l'illustration 9.6 donne une idée de là où nous allons — la page de profil de l'utilisateur, avec des liens de navigation.5 Même si cela n'est pas évident du premier regard, parvenir à ce résultat requiert quelques-uns des défis de programmation Ruby les plus avancés, aussi, accrochez-vous jusqu'au bout et préparez-vous à soulever du poids (ou à en prendre à force de libérer votre frustration sur les paquets de chips. NdT). Heureusement, la première étape est facile — compléter l'action create du contrôleur Sessions est un jeu d'enfant. Malheureusement, c'est aussi une escroquerie.

signin_success_mockup
Illustration 9.6: Une maquette du profil utilisateur après une identification réussie (avec des liens de navigation actualisés).  (taille normale)

9.3.1 L'action create achevée

Remplir l'aire non occupée pour le moment par le commentaire d'identification (extrait 9.8) est simple : à la réussite de l'identification, nous identifions l'utilisateur en utilisant la fonction sign_in et nous le re-dirigeons vers sa page de profil (extrait 9.9). Nous voyons maintenant pourquoi c'est une escroquerie : hélas, sign_in n'existe pas pour le moment. L'écrire nous occupera pendant toute cette section.

Extrait 9.9. L'action create du contrôleur Sessions achevée (mais pas encore fonctionnelle).
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.authenticate(params[:session][:email],
                             params[:session][:password])
    if user.nil?
      flash.now[:error] = "Combinaison Email/Mot de passe invalide."
      @titre = "S'identifier"
      render 'new'
    else
      sign_in user
      redirect_to user
    end
  end
  .
  .
  .
end

Même si la fonction sign_in fait défaut, nous pouvons quand même écrire les tests (extrait 9.10) (nous remplirons le corps du premier test à la section 9.3.3).

Extrait 9.10. Ajouter des tests pour l'identification de l'utilisateur (sera complet à la section 9.3.3).
spec/controllers/sessions_controller_spec.rb
describe SessionsController do
  .
  .
  .
  describe "POST 'create'" do
    .
    .
    .
    describe "avec un email et un mot de passe valides" do

      before(:each) do
        @user = Factory(:user)
        @attr = { :email => @user.email, :password => @user.password }
      end

      it "devrait identifier l'utilisateur" do
        post :create, :session => @attr
        # Remplir avec les tests pour l'identification de l'utilisateur.
      end

      it "devrait rediriger vers la page d'affichage de l'utilisateur" do
        post :create, :session => @attr
        response.should redirect_to(user_path(@user))
      end
    end
  end
end

Ces tests ne réussissent pas encore, mais c'est une bonne base.

9.3.2 Se souvenir de moi

Nous sommes maintenant en mesure de commencer à implémenter notre modèle d'identification, nommément, en se souvenant de l'état « pour toujours » de l'identification de l'utilisateur et en effaçant la session seulement quand l'utilisateur se déconnecte explicitement de lui-même. Les fonctions d'identification elles-mêmes finiront par franchir la ligne du traditionnel Modèle-Vue-Contrôleur ; en particulier, plusieurs fonctions d'identification auront besoin d'être accessibles dans les contrôleurs tout comme dans les vues. Vous vous souvenez sans doute, section 4.2.5, que Ruby fournit un module pratique pour emballer les fonctions ensemble et les inclure à plusieurs endroits, et c'est ce que nous projetons pour les fonctions d'identification. Nous pourrions faire un tout nouveau module pour l'authentification, mais le contrôleur Sessions nous arrive déjà équipé d'un module, nommément SessionsHelper. Plus encore, les helpers sont automatiquement inclus dans les vues Rails, donc tout ce que nous avons besoin de faire pour utiliser les fonctions de l'helper Sessions dans les contrôleurs est d'inclure le module dans le contrôleur Application (extrait 9.11).

Extrait 9.11. Inclure le module helper Sessions dans le contrôleur Application.
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery
  include SessionsHelper
end

Par défaut, tous les helpers sont accessibles dans les vues (views) mais pas dans les contrôleurs. Nous avons besoin des méthodes de l'helper Sessions aux deux endroits, donc nous devons l'inclure explicitement.

Nous sommes maintenant prêts pour le premier élément d'identification, la fonction sign_in elle-même. Notre méthode d'authentification consiste à placer un rappel symbolique (remember token) comme cookie sur le navigateur de l'utilisateur (Box 9.2), et de l'utiliser ensuite pour trouver l'enregistrement de l'utilisateur dans la base de données chaque fois que l'utilisateur navigue de page en page (implémentée à la section 9.3.3). Le résultat (extrait 9.12) pousse deux choses dans la pile : la table cookies et current_user.6 Laissons-les maintenant s'exprimer…

Extrait 9.12. La fonction sign_in complète (mais pas encore fonctionnelle).
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    cookies.permanent.signed[:remember_token] = [user.id, user.salt]
    self.current_user = user
  end
end

L'extrait 9.12 introduit l'utilitaire cookies fourni par Rails. Nous pouvons utiliser cookies comme si c'était une table ; chaque élément dans le cookie est lui-même une table de deux éléments, une value (une valeur) et une date optionnelle expires. Par exemple, nous pourrions implémenter l'identification de l'utilisateur en plaçant un cookie avec une valeur égale à l'id de l'utilisateur qui expire dans vingt ans :

cookies[:remember_token] = { :value   => user.id,
                             :expires => 20.years.from_now.utc }

(Ce code utilise l'un des helpers de temps pratique de Rails, comme discuté dans la Box 9.3.) Nous pourrions alors récupérer l'utilisateur avec un code comme :

User.find_by_id(cookies[:remember_token])

Bien sûr, cookies n'est pas vraiment une table, puisque renseigner un cookie, en fait, enregistre une petite pièce de texte sur le navigateur (comme vu dans l'illustration 9.7), mais une part de la beauté de Rails est qu'il vous laisse oublier ce genre de détail et se concentre sur l'écriture de l'application.

user_remember_token_cookie_rails_3
Illustration 9.7: Un rappel symbolique sécurisé. (taille normale)

Malheureusement, utiliser l'id de l'utilisateur de cette manière n'est pas très sûr, pour des raisons discutées dans le Box 9.2 : un utilisateur malveillant pourrait simuler un cookie avec l'id donnée, et ainsi permettre l'accès du système à n'importe quel utilisateur. La solution traditionnelle avant Rails 3 était de créer un rappel symbolique sécurisé associé au modèle User à utiliser à la place de l'id de l'utilisateur (voyez par exemple la version Rails 2.3 du Tutoriel Rails). Cette façon de faire est devenue si courante que Rails 3 l'implémente maintenant pour nous en utilisant cookies.permanent.signed :

cookies.permanent.signed[:remember_token] = [user.id, user.salt]

L'assignement de la valeur du côté droit est un tableau consistant en un identifiant unique (c'est-à-dire l'id de l'utilisateur) et une valeur sécurisée utilisée pour créer une signature digitale pour empêcher le genre d'attaque décrite à la section 7.2. En particulier, puisque nous avons pris la peine de créer un salt sécurisé à la section 7.2.3, nous pouvons réutiliser cette valeur ici pour signer le rappel symbolique. Sous le capot, utiliser permanent fait que Rails règle l'expiration à 20.years.from_now, et signed rend le cookie sécurisé, de telle sorte que l'id de l'utilisateur n'est jamais exposé dans le navigateur (nous verrons comment récupérer l'utilisateur en utilisant le rappel symbolique à la section 9.3.3).

Le code ci-dessus montre l'importance d'utiliser new_record? dans l'extrait 7.10 pour sauver le sel seulement à la création de l'utilisateur. Dans le cas contraire, ce salt changerait chaque fois que l'utilisateur est enregistré, empêchant de récupérer la session de l'utilisateur de la section 9.3.3.

9.3.3 Utilisateur courant

Dans cette section, nous apprendrons comment obtenir et définir la session de l'utilisateur courant. Regardons à nouveau la fonction sign_in pour voir où nous en sommes :

module SessionsHelper

  def sign_in(user)
    cookies.permanent.signed[:remember_token] = [user.id, user.salt]
    self.current_user = user
  end
end

Concentrons-nous maintenant sur la deuxième ligne :7

self.current_user = user

Le but de cette ligne est de créer une variable current_user, accessible aussi bien dans les contrôleurs que dans les vues, ce qui permettra des constructions telles que :

<%= current_user.nom %>

… ou :

redirect_to current_user

Le but principal de cette section est de définir current_user.

Pour décrire le comportement de la machinerie d'identification restant à implémenter, nous allons d'abord remplir le test pour l'identification de l'utilisateur (extrait 9.13).

Extrait 9.13. Remplir le test pour l'identification de l'utilisateur.
spec/controllers/sessions_controller_spec.rb
describe SessionsController do
  .
  .
  .
  describe "POST 'create'" do
    .
    .
    .
    describe "avec un email et un mot de passe valides" do

      before(:each) do
        @user = Factory(:user)
        @attr = { :email => @user.email, :password => @user.password }
      end

      it "devrait identifier l'utilisateur" do
        post :create, :session => @attr
        controller.current_user.should == @user
        controller.should be_signed_in
      end

      it "devrait rediriger vers la page d'affichage de l'utilisateur" do
        post :create, :session => @attr
        response.should redirect_to(user_path(@user))
      end
    end
  end
end

Le nouveau test utilise la variable controller (contrôleur) (accessible à l'intérieur des tests Rails) pour vérifier que la variable current_user est réglée à l'utilisateur identifié, et que l'utilisateur est identifié :

it "devrait identifier l'utilisateur" do
  post :create, :session => @attr
  controller.current_user.should == @user
  controller.should be_signed_in
end

La deuxième ligne peut être un peu déroutante à ce stade, mais vous pouvez deviner en vous fondant sur la convention RSpec pour les méthodes booléenne que :

controller.should be_signed_in

… est équivalent à :

controller.signed_in?.should be_true

C'est une allusion au fait que nous devrons définit une méthode signed_in? qui retourne true (vrai) si un utilisateur est identifié et false (faux) dans le cas contraire. Plus encore, la méthode signed_in? sera attachée au contrôleur, pas à l'utilisateur, ce qui explique pourquoi nous écrivons controller.signed_in? au lieu de current_user.signed_in? (si aucun utilisateur n'est identifié, comment pourrions-nous appelé signed_in? sur lui ?).

Pour commencer à écrire le code de current_user, notez que la ligne :

self.current_user = user

… est un assignement. Ruby possède une syntaxe spéciale pour définir de telle fonction d'assignement, montré dans l'extrait 9.14.

Extrait 9.14. Définir l'assignement de current_user.
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    .
    .
    .
  end

  def current_user=(user)
    @current_user = user
  end
end

Cela peut sembler déroutant, mais ça définit simplement une méthode current_user= expressément conçue pour traiter l'assignement de current_user. Son premier argument est la partie droite de l'assignement, dans ce cas l'utilisateur qui doit être identifié. Le corps d'une ligne de cette méthode définit juste une variable d'instance @current_user, en enregistrant effectivement l'utilisateur pour un usage ultérieur.

En Ruby ordinaire, nous pourrions définir une deuxième méthode, current_user, conçue pour retourner la valeur de @current_user (extrait 9.15).

Extrait 9.15. Un définition tentante mais inutile de current_user.
module SessionsHelper

  def sign_in(user)
    .
    .
    .
  end

  def current_user=(user)
    @current_user = user
  end

  def current_user
    @current_user     # Inutile ! N'utilisez pas cette ligne.
  end
end

Si nous faisions ça, nous répliquerions en effet la fonctionnalité de attr_accessor, vue pour la première fois à la section 4.4.5 et utilisée pour créer un password (mot de passe) virtuel à la section 7.1.1.8 Le problème est que ça échoue complètement pour résoudre notre problème : avec le code de l'extrait 9.15, l'état de l'identification de l'utilisateur serait oublié : dès que l'utilisateur rejoindrait une autre page — poof ! — la session cesserait et l'utilisateur serait automatiquement déconnecté.

Pour éviter ce problème, nous pouvons trouver la session utilisateur correspondant au cookie créé par le code de l'extrait 9.12, comme montré dans l'extrait 9.16.

Extrait 9.16. Trouver l'utilisateur courant par remember_token.
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def current_user
    @current_user ||= user_from_remember_token
  end

  private

    def user_from_remember_token
      User.authenticate_with_salt(*remember_token)
    end

    def remember_token
      cookies.signed[:remember_token] || [nil, nil]
    end
end

Ce code utilise plusieurs fonctionnalités plus avancées de Ruby, donc prenons un moment pour les examiner.

D'abord, l'extrait 9.16 utilise l'opérateur d'assignement courant mais pour le moment obscur ||= (« ou égal ») (Box 9.4). Son effet est de régler la variable d'instance à l'utilisateur correspondant au rappel symbolique, mais seulement si @current_user n'est pas défini.9 En d'autres mots, la construction :

@current_user ||= user_from_remember_token

… appelle la méthode user_from_remember_token la première fois que current_user est appelé, mais retourne @current_user au cours des invocations suivantes sans appeler user_from_remember_token.10

L'extrait 9.16 utilise aussi l'opérateur *, qui nous permet d'utiliser un tableau à deux éléments comme argument pour une méthode attendant deux variables, comme nous pouvons le voir dans la session de console :

$ rails console
>> def foo(bar, baz)
?>   bar + baz
?> end
=> nil
>> foo(1, 2)
=> 3
>> foo(*[1, 2])
=> 3

La raison pour laquelle c'est nécessaire dans l'extrait 9.16 est que cookies.signed[:remember_me] retourne un tableau de deux éléments — l'id de l'utilisateur et le salt — mais, (en suivant les conventions de Ruby) nous voulons que la méthode authenticate_with_salt prenne deux arguments, donc elle peut être invoquée avec :

User.authenticate_with_salt(id, salt)

(Il n'existe pas de raison fondamentale pour que authenticate_with_salt ne puisse prendre un tableau comme argument, mais ce ne serait pas idiomatiquement correct en Ruby.)

Enfin, dans la méthode d'helper remember_token définie par l'extrait 9.16, nous utilisons l'opérateur || pour retourner un tableau de valeurs nulles si cookies.signed[:remember_me] lui-même est nul :

cookies.signed[:remember_token] || [nil, nil]

La raison d'être de ce code est que le support pour les tests des cookies d'identification est encore jeune, et la valeur nil pour le cookie occasionne des cassures de test fallacieux. Retourner plutôt [nil, nil] règle ce problème.11

L'étape finale pour obtenir que le code de l'extrait 9.16 fonctionne est de définir une méthode de classe authenticate_with_salt. Cette méthode, qui est analogue à la méthode originale authenticate définie dans l'extrait 7.12, est montrée dans l'extrait 9.17.

Extrait 9.17. Ajout d'une méthode authenticate_with_salt au modèle User.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .

  def self.authenticate(email, submitted_password)
    user = find_by_email(email)
    return nil  if user.nil?
    return user if user.has_password?(submitted_password)
  end

  def self.authenticate_with_salt(id, cookie_salt)
    user = find_by_id(id)
    (user && user.salt == cookie_salt) ? user : nil
  end
  .
  .
  .
end

Ici, authenticate_with_salt commence par trouver l'utilisateur par son id unique, et vérifie alors que le salt enregistré dans le cookie est bien celui de l'utilisateur.

Il est important de noter que cette implémentation de authenticate_with_salt est identique à la fonction du code suivant, qui est plus proche de la méthode authenticate :

def self.authenticate_with_salt(id, cookie_salt)
  user = find_by_id(id)
  return nil  if user.nil?
  return user if user.salt == cookie_salt
end

Dans les deux cas, la méthode retourne l'utilisateur si user est pas nil et si le sel de l'utilisateur correspond au sel du cookie, et retourne nil dans le cas contraire. D'un autre côté, le code tel que :

(user && user.salt == cookie_salt) ? user : nil

… est courant en Ruby idiomatique correct, donc j'ai pensé que c'était une bonne idée de l'introduire. Ce code utilise l'étrange mais utile ternary operator (opérateur ternaire) pour « compresser » une construction if-else en une seule ligne (Box 9.5).

À ce point où nous en sommes, le test d'identification réussit presque ; la seule chose restant à faire est de définir la méthode booléenne signed_in?. Heureusement, c'est facile avec l'utilisation de l'opérateur « not » (pas! : un utilisateur est identifié si current_user n'est pas nil (extrait 9.18).

Extrait 9.18. La méthode d'helper signed_in?.
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def signed_in?
    !current_user.nil?
  end

  private
  .
  .
  .
end

Bien qu'elle soit déjà utile pour le test, nous allons revoir la méthode signed_in? pour un meilleur usage encore à la section 9.4.3 et encore au chapitre 10.

Avec ça, tous les tests devraient réussir.

9.4 Déconnexion

Comme discuté à la section 9.1, notre modèle d'authentification est de garder les utilisateurs identifiés jusqu'à ce qu'ils se déconnectent explicitement. Dans cette section, nous allons ajouter les fonctionnalités nécessaires à la déconnexion. Une fois cela fait, nous ajouterons quelques tests d'intégration pour mettre à l'épreuve notre machinerie de déconnexion.

9.4.1 Détruire les sessions

Jusqu'ici, les actions du contrôleur Sessions ont suivi la convention REST en utilisant new pour une page d'identification et create pour achever l'identification. Nous allons poursuivre sur cette voie en utilisant une action destroy pour effacer la sessions, c'est-à-dire pour se déconnecter.

Pour tester l'action déconnexion, nous avons d'abord besoin d'un moyen de s'identifier dans le test. La façon la plus facile de faire ça est d'utiliser l'objet controller vu à la section 9.3.3 et d'utiliser l'helper sign_in pour identifier l'utilisateur donné. Pour pouvoir utiliser la fonction test_sign_in en résultant dans tous nos tests, nous devons la placer dans le fichier helper spec, comme vu dans l'extrait 9.19.12

Extrait 9.19. Une fonction test_sign_in pour simuler l'identification de l'utilisateur à l'intérieur des tests.
spec/spec_helper.rb
.
.
.
Rspec.configure do |config|
  .
  .
  .
  def test_sign_in(user)
    controller.sign_in(user)
  end
end

Après avoir joué test_sign_in, current_user ne sera pas nil, donc signed_in? renverra true (vrai).

Avec cet helper spec en main, le test pour la déconnexion est simple : s'identifier avec un utilisateur (d'usine) et alors invoquer l'action destroy et vérifier que l'utilisateur se trouve déconnecté (extrait 9.20).

Extrait 9.20. Un test de destruction de la session (déconnexion utilisateur).
spec/controllers/sessions_controller_spec.rb
describe SessionsController do
  .
  .
  .
  describe "DELETE 'destroy'" do

    it "devrait déconnecter un utilisateur" do
      test_sign_in(Factory(:user))
      delete :destroy
      controller.should_not be_signed_in
      response.should redirect_to(root_path)
    end
  end
end

Le seul élément nouveau ici est la méthode delete, qui répond à la requête HTTP DELETE (en analogie aux méthodes get et post vues dans les précédents tests), comme l'exigent les conventions REST (Table 9.1).

Comme avec l'identification de l'utilisateur, qui est reliée à la fonction sign_in, la déconnexion de l'utilisateur confie juste le dur boulot à la fonction sign_out (extrait 9.21).

Extrait 9.21. Détruire une session (déconnexion utilisateur).
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    sign_out
    redirect_to root_path
  end
end

Comme avec les autres éléments d'authentification, nous placerons sign_out dans le module helper Sessions (extrait 9.22).

Extrait 9.22. La méthode sign_out dans le module helper Sessions.
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    cookies.permanent.signed[:remember_token] = [user.id, user.salt]
    self.current_user = user
  end
  .
  .
  .
  def sign_out
    cookies.delete(:remember_token)
    self.current_user = nil
  end

  private
    .
    .
    .
end

Comme vous pouvez le voir, la méthode sign_out effectivement annule la méthode sign_in en effaçant le rappel symbolique (remember token) et en réglant l'utilisateur courant à nil.13

9.4.2 Connexion à l'inscription

En principe, nous en avons fini avec l'authentification, mais telle que se présente l'application actuellement, il n'y a pas de liens pour les actions d'identification et de déconnexion. Plus encore, les tout nouveaux utilisateurs enregistrés peuvent être déroutés de ne pas être identifiés par défaut à l'inscription.

Nous allons fixer ce second problème d'abord, en commençant par tester qu'un nouvel utilisateur est automatiquement identifié (extrait 9.23).

Extrait 9.23. Tester qu'un nouvel utilisateur soit aussi identifié.
spec/controllers/users_controller_spec.rb
require 'spec_helper'

describe UsersController do
  render_views
  .
  .
  .
  describe "POST 'create'" do
    .
    .
    .
    describe "success" do
      .
      .
      .
      it "devrait identifier l'utilisateur" do
        post :create, :user => @attr
        controller.should be_signed_in
      end
      .
      .
      .
    end
  end
end

Avec la méthode sign_in de la section 9.3, faire que le test réussisse en identifiant un utilisateur est facile : ajoutez juste sign_in @user juste après avoir sauver l'utilisateur dans la base de données (extrait 9.24).

Extrait 9.24. Identifier l'utilisateur après son inscription.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(params[:user])
    if @user.save
      sign_in @user
      flash[:success] = "Bienvenue dans l'Application Exemple !"
      redirect_to @user
    else
      @titre = "Sign up"
      render 'new'
    end
  end

9.4.3 Changement des liens de la mise en page

Nous en arrivons finalement à une application fonctionnelle pour tous nos travaux d'identification et d'inscription : nous allons changer les liens du layout selon l'état de l'identification. En particulier, comme le montre la maquette de l'illustration 9.6, nous allons faire en sorte que les liens changent suivant que l'utilisateur est identifié ou déconnecté, et nous allons ajouter aussi un lien vers la page de profil pour un utilisateur identifié.

Nous commençons avec les deux tests d'intégration : un pour vérifier que le lien "S'inscrire" est visible pour un utilisateur non identifié, et un pour vérifier que le lien Déconnexion soit visible pour un utilisateur identifié ; ces deux cas vérifient que le lien conduise à l'URL appropriée. Nous placerons ces tests dans le test des liens du layout que nous avons créé à la section 5.2.1 ; le résultat apparait dans l'extrait 9.25.

Extrait 9.25. Tests pour les liens de connexion/déconnexion dans le layout du site.
spec/requests/layout_links_spec.rb
describe "Liens du layout" do
  .
  .
  .
  describe "quand pas identifié" do
    it "doit avoir un lien de connexion" do
      visit root_path
      response.should have_selector("a", :href => signin_path,
                                         :content => "S'identifier")
    end
  end

  describe "quand identifié" do

    before(:each) do
      @user = Factory(:user)
      visit signin_path
      fill_in :email,    :with => @user.email
      fill_in "Mot de passe", :with => @user.password
      click_button
    end

    it "devrait avoir un lien de déconnxion" do
      visit root_path
      response.should have_selector("a", :href => signout_path,
                                         :content => "Déconnexion")
    end

    it "devrait avoir un lien vers le profil" 
  end
end

Ici le bloc before(:each) identifie en visitant la page d'identification et en soumettant une paire email/mot de passe valide.14 Nous faisons cela plutôt que d'utiliser la fonction test_sign_in de l'extrait 9.19 parce que test_sign_in ne fonctionne pas à l'intérieur des tests d'intégration pour certaines raisons (voir la section 9.6 pour un exercice pour construire une fonction integration_sign_in à l'usage des tests d'intégration).

Le code de l'application utilise une structure de branchement si-alors à l'intérieur du code Ruby embarqué, en utilisant la méthode signed_in? définie dans l'extrait 9.18 :

<% if signed_in? %>
<li><%= link_to "Déconnexion", signout_path, :method => :delete %></li>
<% else %>
<li><%= link_to "S'identifier", signin_path %></li>
<% end %>

Remarquez que le lien de déconnexion passe un argument table indiquant qu'il devrait soumettre avec une requête HTTP DELETE.15 Avec ce fragment ajouté, l'entête complète du partiel apparait comme dans l'extrait 9.26.

Extrait 9.26. Changer les liens du layout links pour l'utilisateur identifié.
app/views/layouts/_header.html.erb
<header>
  <%= link_to logo, root_path %>
  <nav class="round">
    <ul>
      <li><%= link_to "Accueil", root_path %></li>
      <li><%= link_to "Aide", help_path %></li>
      <% if signed_in? %>
      <li><%= link_to "Déconnexion", signout_path, :method => :delete %></li>
      <% else %>
      <li><%= link_to "S'identifier", signin_path %></li>
      <% end %>
    </ul>
  </nav>
</header>

Dans l'extrait 9.26 nous avons utilisé l'helper logo des exercices du chapitre 5 (section 5.5) ; dans le cas où vous n'auriez pas fait cet exercice, la réponse apparait dans l'extrait 9.27.

Extrait 9.27. Un helper pour le logo du site.
app/helpers/application_helper.rb
module ApplicationHelper
  .
  .
  .
  def logo
    image_tag("logo.png", :alt => "Application Exemple", :class => "round")
  end
end

Enfin, ajoutons le lien vers le profil. Le test (extrait 9.28) et le code de l'application (extrait 9.29) sont tous deux extrêmement simples. Remarquez que l'URL du lien vers le profil est simplement current_user,16 qui est notre première utilisation de cette méthode utile (ça ne sera pas la dernière).

Extrait 9.28. Un test pour le lien du profil.
spec/requests/layout_links_spec.rb
describe "Liens du layout" do
  .
  .
  .
  describe "quand identifié" do
    .
    .
    .
    it "devrait avoir un lien vers le profil" do
      visit root_path
      response.should have_selector("a", :href => user_path(@user),
                                         :content => "Profil")
    end
  end
end
Extrait 9.29. Ajout du lien vers le profil.
app/views/layouts/_header.html.erb
<header>
  <%= link_to logo, root_path %>
  <nav class="round">
    <ul>
      <li><%= link_to "Accueil", root_path %></li>
      <% if signed_in? %>
      <li><%= link_to "Profil", current_user %></li>
      <% end %>
      <li><%= link_to "Aide", help_path %></li>
      <% if signed_in? %>
      <li><%= link_to "Déconnexion", signout_path, :method => :delete %></li>
      <% else %>
      <li><%= link_to "S'identifier", signin_path %></li>
      <% end %>
    </ul>
  </nav>
</header>

Avec le code de cette section, un utilisateur identifié voit maintenant les deux liens de déconnexion et de profil, comme voulu (illustration 9.8).

profile_with_signout_link
Illustration 9.8: Un utilisateur identifié avec les liens déconnexion et profil (version anglaise). (taille normale)

9.4.4 Test d'intégration pour l'identification et la déconnexion

Pierre angulaire de notre dur labeur sur l'authentification, nous allons finir avec les tests d'intégration pour l'identification et la déconnexion (placé dans le fichier users_spec.rb pour le côté pratique). Le testing d'intégration RSpec est suffisamment expressif pour que l'extrait 9.30 ne demande qu'une toute petite explication ; j'aime spécialement l'utilisation de click_link "Déconnexion", qui ne fait pas que simuler le clic sur le lien de déconnexion dans le navigateur mais produit également une erreur si ce lien n'existe pas — testant ainsi l'URL, la route nommée, le texte du lien et le changement des liens dans le laxyout en une seule ligne. Si ça n'est pas un test d'integration, je ne sais pas ce que c'est !

Extrait 9.30. Un test d'intégration pour l'identification et la déconnxion.
spec/requests/users_spec.rb
require 'spec_helper'

describe "Users" do

  describe "signup" do
    .
    .
    .
  end

  describe "identification/déconnexion" do

    describe "l'échec" do
      it "ne devrait pas identifier l'utilisateur" do
        visit signin_path
        fill_in "eMail",    :with => ""
        fill_in "Mot de passe", :with => ""
        click_button
        response.should have_selector("div.flash.error", :content => "Invalid")
      end
    end

    describe "le succès" do
      it "devrait identifier un utilisateur puis le déconnecter" do
        user = Factory(:user)
        visit signin_path
        fill_in "eMail",    :with => user.email
        fill_in "Mot de passe", :with => user.password
        click_button
        controller.should be_signed_in
        click_link "Déconnexion"
        controller.should_not be_signed_in
      end
    end
  end
end

9.5 Conclusion

Nous avons couvert un grand nombre de points dans ce chapitre, transformant notre application prometteuse mais informe en site capable d'accomplir une pleine suite de registration et d'identification. Tout ce qu'il reste à faire pour achever l'authentification est de restreindre l'accès aux pages en s'appuyant sur l'état de l'identification et l'identité de l'utilisateur. Nous accomplirons ces tâches tout en donnant à l'utilisateur la possibilité de modifier ses informations et en donnant aux administrateurs la faculté de supprimer des utilisateurs.

Mais avant de poursuivre, fusionnons nos changements dans la branche maitresse de notre repository :

$ git add .
$ git commit -m "Fini avec l'identification et la deconnexion"
$ git checkout master
$ git merge sign-in-out

9.6 Exercises

Le deuxième et troisième exercices sont plus difficiles que d'habitude. Les résoudre nécessitera des recherches externes (par exemple la consultation de l'API Rails et des recherches Google), mais ils peuvent être sautés sans perte de la continuité du livre.

  1. Plusieurs des spécifications d'intégration utilisent le même code pour identifier un utilisateur. Remplacez ce code par une fonction integration_sign_in dans l'extrait 9.31 et vérifiez que les tests continuent de réussir.
  2. Utilisez la session plutôt que les cookies de telle sorte que les utilisateurs puissent être automatiquement déconnectés quand ils ferment leur navigateur..17 Astuce : faites une recherche Google sur les termes « Rails session ».
  3. (avancé) Certains sites utilisent une connexion sécurisée (HTTPS) pour leurs pages d'identification. Cherchez en ligne pour apprendre comment utiliser HTTPS en Rails, et sécurisez ensuite les actions new et create du contrôleur Sessions. Challenge supplémentaire : écrivez des tests pour les fonctionnalités HTTPS (note : je vous suggère de faire cet exercice seulement en mode développement, ce qui ne nécessitera pas d'obtenir un certificat SSL ou de régler la machinerie de cryptage SSL. En réalité, déployer un site « SSL-enabled » est beaucoup plus difficile).
Extrait 9.31. Une fonction pour identifier les utilisateurs à l'intérieur des tests d'intégration.
spec/spec_helper.rb
.
.
.
Rspec.configure do |config|
  .
  .
  .
  def test_sign_in(user)
    controller.sign_in(user)
  end

  def integration_sign_in(user)
    visit signin_path
    fill_in "eMail",    :with => user.email
    fill_in "Mot de passe", :with => user.password
    click_button
  end
end
  1. Un autre modèle courant est de faire expirer la session après un certain laps de temps. C'est spécialement approprié sur les sites qui contiennent des données sensibles, comme les banques et les comptes financiers. 
  2. Nous verrons à la section 9.3.2 combien de temps signifie en réalité « pour toujours ». 
  3. Si on lui donne les actions create et destroy, le script generate construira les vues pour ces actions, ce dont nous n'avons pas besoin. Bien sûr, nous pourrions détruire ces vues, mais j'ai décidé de les omettre de la commande generate et de plutôt définir ces actions à la main. 
  4. Dans le cas où vous vous demanderiez pourquoi nous utilisons user plutôt que @user dans l'extrait 9.8, c'est parce que cette variable user n'est jamais demandée dans aucune vue, donc il n'y a pas de raison d'utiliser une variable d'instance ici (utiliser @user fonctionnerait aussi, cependant). 
  5. Image provenant de http://www.flickr.com/photos/hermanusbackpackers/3343254977/
  6. Sur certains systèmes, vous pouvez avoir besoin d'utiliser self.current_user = user pour faire réussir les tests à venir. 
  7. Parce que le module helper Sessions est inclus dans le contrôleur Application, la variable self ici est le contrôleur lui-même. 
  8. En fait, les deux sont exactement équivalents ; attr_accessor est simplement une façon pratique de créer automatiquement des méthodes telles que getter et setter. 
  9. Classiquement, cela signifie assigner les valeurs qui sont intialement nil, mais notez que les valeurs false seront également sur-définies par l'opérateur ||=
  10. Cette technique d'optimisation pour éviter de répéter des appels de fonction est connue sous le noim de mémoization
  11. Cela ressemble à l'histoire de la queue qui remue le chien, mais c'est le prix à payer pour rester à la pointe. 
  12. Si vous utilisez Spork, elle sera placée à l'intérieur du bloc Spork.prefork
  13. Vous pouvez apprendre beaucoup sur des choses telles que cookies.delete en lisant l'entrée « cookies » dans l'API Rails (puisque les liens vers l'API Rails tendent à être très vite dépassés, utilisez votre moteur de recherche pour trouver une version actualisée). 
  14. Notez que nous pouvons utiliser les symboles à la place des chaines de caractères pour les labels, par exemple fill_in :email au lieu de fill_in "Email". Nous utilisons ce dernier dans l'extrait 8.22, mais maintenant cela ne devrait pas vous surprendre que Rails nous permette d'utiliser plutôt les symboles. 
  15. Les navigateurs web ne peuvent pas, en vérité, transmettre une requête DELETE ; Rails la simule avec JavaScript. 
  16. De la section 7.3.3 vous vous souvenez que nous pouvons nous lier directement à un objet utilisateur et permettre à Rails de deviner l'URL adéquate. 
  17. Ce qui est un peu déroutant, c'est que nous avons utilisé les cookies pour implémenter les sessions, et la session est implémentée avec les cookies !