Développer une application web avec authentification, persistance des données, synchronisation entre plusieurs utilisateurs en temps réel et tout cela sans avoir à écrire une ligne de code côté serveur.
Vous penseriez cela impossible ? Et pourtant, avec Firebase c’est possible !
Je vous propose un tutoriel pour faire un petit tour de la technologie. Après une brève présentation des concepts de Firebase et AngularFire, nous développerons pas à pas une application de gestion de listes d’envies. Nous aborderons les différents types de synchronisation des données, l’authentification et la mise en place de règles de sécurité sur une application de gestion de wishlists.
Disclaimer : Une connaissance des bases du framework AngularJS sont requises pour bien comprendre ce tutoriel.
Firebase – kezako ?
Firebase est une solution SaaS qui prend en charge la partie serveur, permettant ainsi aux développeurs de créer des applications web uniquement côté client. L’intégration des services offerts par Firebase passe par un ensemble de librairies clientes proposées aux développeurs. Elle s’occupe de la persistance des données, et de leur synchronisation en temps réel avec les clients. Cerise sur le gâteau, elle fournit une gestion complète de la sécurité et de l’authentification. Firebase est scalable, permettant à votre application de supporter un grand nombre d’utilisateurs connectés simultanément, sans dégradation de performances.
La fonctionnalité probablement la plus spectaculaire de Firebase est la synchronisation automatique des données entre les clients et le serveur Firebase. Si un client change une partie des données, tous les autres clients observent cette modification de leur côté, quasi instantanément, et cela, même sous une charge importante. Dans le cas où le client perd temporairement la connexion réseau, l’application reste entièrement fonctionnelle. Elle va synchroniser et, si nécessaire, fusionner les données, une fois la connexion rétablie.
La sécurité a été une des priorités majeures pour les concepteurs. La stack fournit une suite complète de méthodes d’authentification, permettant aux développeurs d’avoir un contrôle total sur l’authentification des utilisateurs. La mise en place de règles de sécurité est rendue simple et flexible par une approche déclarative très intéressante. De plus, tout le trafic transite par des canaux SSL, assurant que les données ne sont jamais lues ou modifiées par un tiers.
Firebase propose une librairie JavaScript, mais aussi des librairies natives pour mobiles iOS, et Android. Enfin, vous pouvez choisir de réaliser votre propre serveur, et d’y ajouter Firebase pour bénéficier de la synchronisation des données en temps réel. En effet, des bibliothèques pour Node.js et Java, conçues pour l’utilisation côté serveur, sont disponibles. Et si par hasard, votre plateforme n’est pas supportée, une API REST vous permet d’intégrer Firebase malgré tout.
Mais assez de théorie.
Une démo !
L’exemple classique est l’implémentation d’un chat, sous vos yeux ébahis, en seulement 14 lignes de code !
http://jsfiddle.net/dimapod/aNKNC/embedded/result/
Ouvrez plusieurs fenêtres ou navigateurs pour voir la synchronisation en temps réel.
// Get a reference to the root of the chat data. var messagesRef = new Firebase('https://xchat.firebaseio.com/); // When the user presses enter on the message input, write the message to firebase. $('#messageInput').keypress(function (e) { if (e.keyCode == 13) { var name = $('#nameInput').val(); var text = $('#messageInput').val(); messagesRef.push({ name: name, text: text }); $('#messageInput').val(''); } }); // Add a callback that is triggered for each chat message. messagesRef.limit(10).on('child_added', function (snapshot) { var message = snapshot.val(); $('<div/>').text(message.text).prepend($('<em/>').text(message.name + ': ')).appendTo($('#messagesDiv')); $('#messagesDiv')[0].scrollTop = $('#messagesDiv')[0].scrollHeight; });
Chaque nouvelle Firebase se voit assigner un hostname unique. Dans notre cas c’est https://xchat.firebaseio.com/. Une des choses géniales avec Firebase est sa Forge : une IHM accessible directement à partir du navigateur.
Elle permet de :
- visualiser les données de votre Firebase ;
- configurer l’authentification et les règles de sécurité ;
- fournir des statistiques utiles pour analyser la performance ;
- plein d’autres choses…
Les données dans Firebase sont représentées sous forme hiérarchique :
Chaque morceau de donnée est accessible par URL. Par exemple xchat.firebaseio.com/-J964rQISfk0fvREql0v/name
mène à “Andrew”, simple et efficace.
La ligne var messagesRef = new Firebase('https://xchat.firebaseio.com/);
permet de récupérer la référence vers une structure de données sous Firebase. Cette référence est par la suite utilisée pour ajouter un nouvel objet, correspondant au message (ligne 8), ainsi que pour s’inscrire à l’événement d’ajout d’un nouveau message (ligne 13) pour y attacher un comportement spécifique (ajouter un élément dans le DOM).
AngularFire
Firebase fonctionne parfaitement avec n’importe quelle framework de Single Page Application ou même juste avec JQuery. Elle fournit des librairies pour l’intégration de Backbone.js, EmberJS et AngularJS.
Dans ce tutoriel, je vais utiliser AngularJS avec AngularFire, une librairie permettant d’étendre le double data binding d’Angular en persistant également dans Firebase. En pratique cela signifie que toute modification du DOM mettra automatiquement à jour le contenu de la base de données Firebase. Inversement, toute modification des données dans la base entraînera la mise à jour automatique du DOM. Et cela, quel que soit le nombre de clients connectés simultanément à votre application : un ou un million (en théorie, si vous avez des benchmarks concret, postez les en commentaire). Firebase prendra en charge toute la problématique de dimensionnement horizontale, sans la moindre intervention de votre part.
Comme vous pouvez le voir, quand les données sont mises à jour dans un de ces 3 endroits : Vue, Modèle, ou Firebase, les modifications sont propagées en temps réel aux deux autres.
Voyons maintenant comment cela fonctionne.
Mise en pratique
Maintenant commençons à remplir notre objectif et développons pas à pas une simple application permettant de gérer une liste d’envies, baptisé YAWL (Yet Another Wish List).
Formulons les fonctionnalités de notre future application à l’aide de quelques user stories :
- En tant qu’utilisateur je veux pouvoir me connecter/déconnecter à l’application afin d‘accéder à l’interface privée
- En tant qu’utilisateur je veux pouvoir créer/supprimer des listes d’envies afin de grouper mes envies
- En tant qu‘utilisateur je veux pouvoir ajouter/supprimer des articles dans mes listes d’envies afin de constituer une liste de cadeaux
- En tant qu‘utilisateur je veux pouvoir générer un lien vers chaque liste d’envies afin de les partager avec mes invités
- En tant qu‘utilisateur invité je veux pouvoir accéder aux listes des autres utilisateurs par un lien afin de voir leurs articles
- En tant qu‘utilisateur invité je veux réserver/relâcher un ou plusieurs articles afin de montrer mon intention d’offrir un cadeau
- En tant qu‘utilisateur je veux que mes listes d’envies ne soient modifiables que par moi afin de protéger mes données
- En tant qu‘utilisateur je veux que mes invités ne puissent pas réserver/relâcher les articles qui ont été déjà réservés par les autres utilisateurs afin d‘éviter de recevoir 2 fois le même cadeau
Alors, allons-y !
La stack
Pour notre application, je vais adapter le principe KISS et essayer de garder l’application la plus simple possible, mais néanmoins complètement fonctionnelle.
Afin de bootstrapper notre application, j’ai pris comme base le projet Angular-seed. A mon sens, le seed est loin d’être le meilleur façon de structurer l’application AngularJS mais, c’est un sujet à part. Pour notre besoin, cette organisation est suffisante. J’utilise la version v1.2.4 d’AngularJS.
Pour styler l’application, je vais me servir du framework Pure css (histoire de changer de l’omniprésent Twitter Bootstrap) avec du LESS.
Pour que l’authentification Firebase fonctionne correctement, l’application doit être servie depuis un serveur. Dans cet exemple, je crée un serveur avec nodejs / express. (si vous ne voulez pas installer node.js, vous pouvez utiliser n’importe quel serveur capable d’exposer les fichiers statiques : apache, nginx, ou autre).
Pour suivre l’écriture de l’application pas à pas, cloner le repo git (git@github.com:xebia-france/yawl.git) et mettez vous sur la branche “start”.
git clone git@github.com:xebia-france/yawl.git -b start
Pour lancer :
cd yawl npm install node server.js
Et allez à la page http://localhost:3000
Structure des répertoires
Comme vous le voyez, rien de particulier.
Pour commencer, ajoutons les librairies de Firebase et AngularFire à côté des autres inclusions dans index.html :
<!-- AngularJS --> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.4/angular.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.4/angular-route.min.js"></script> <!-- Firebase --> <script src="//cdn.firebase.com/v0/firebase.js"></script> <script src="//cdn.firebase.com/v0/firebase-simple-login.js"></script> <script src="//cdn.firebase.com/libs/angularfire/0.5.0/angularfire.min.js"></script>
Et ajoutons ensuite le module Firebase en tant que dépendance de notre application AngularJS dans le fichier app.js :
angular.module('yawl', [ 'firebase', 'ngRoute', 'yawl.controllers.header', … ]);
Si vous n’avez pas encore de compte chez Firebase, il est grand temps de vous enregistrer ici, créer votre propre firebase et renseigner son URL dans une constante AngularJS que l’on utilisera plus tard :
angular.module('yawl').constant('FBURL', 'https://<<votre firebase name>>.firebaseio.com')
Données, données, données…
Les données sont au centre d’une application utilisant Firebase. Il est crucial de les structurer dès la conception de votre application de telle manière que cette structure puisse répondre à tous les cas fonctionnels de votre application : la manipulation des données, la recherche et la protection.
Notre application aura une structure de données assez simple :
Nous verrons par la suite que cette structure est parfaitement adaptée pour remplir les fonctionnalités de l’application, y compris pour définir les autorisations inter utilisateurs.
Authentification avec Firebase
User story :
- En tant qu’utilisateur je veux pouvoir me connecter/déconnecter à l’application afin d‘accéder à l’interface privée.
Tout d’abord nous déclarons la route vers la page de login :
$routeProvider.when('/login', {templateUrl: 'partials/login.tpl.html'});
Il est évidemment impensable de laisser l’intégralité de l’authentification seulement côté client, étant donné que l’on doit assumer que le client a le contrôle total sur son environnement d’exécution. Firebase fournit un module spécifiquement conçu pour l’authentification : FirebaseSimpleLogin. Ce module nous offre l’authentification par l’intermédiaire de Facebook, Twitter, Github ou une authentification personnalisée. Il est même possible d’intégrer des mécanismes d’authentification existants si vous avez déjà un serveur gérant l’authentification.
AngularFire fait l’abstraction de FirebaseSimpleLogin avec le module $firebaseAuth
angular.module('yawl').run(['$rootScope', '$firebaseAuth', 'FBURL', 'Firebase', function ($rootScope, $firebaseAuth, FBURL, Firebase) { $rootScope.auth = $firebaseAuth(new Firebase(FBURL), { path: '/login' }); }]);
Comme vous pouvez le voir dans le code ci-dessus, pour initialiser $firebaseAuth
il faut lui passer la référence vers la racine de nos données dans Firebase. En retour on aura un objet d’authentification qui sera rattaché à $rootScope.auth
. Quand utilisateur va s’identifier, les informations d’authentification sur l’utilisateur connecté seront misent dans $rootScope.auth.user
. Quand l’utilisateur se déconnecte – $rootScope.auth.user
n’existe plus. Notons aussi, que le 'path'
définit la route vers laquelle $firebaseAuth
re-dirigera l’application pour authentifier l’utilisateur.
Nous pouvons maintenant définir un contrôleur permettant d’authentifier les utilisateurs loginCtrl.js :
angular.module('yawl.controllers.login', []). controller('loginCtrl', ['$rootScope', '$location', function ($rootScope, $location) { this.loginWith = function (provider) { $rootScope.auth.$login(provider); }; $rootScope.$on("user:logout", function () { $rootScope.auth.$logout(); $location.path('/login'); }); }]);
Le template « login.tpl.html » est déjà prêt. Voici une version simplifiée pour illustrer son fonctionnement :
<h2>SIGN IN</h2> <div ng-controller="loginCtrl as login" > <button ng-click="login.loginWith('github')">Login with Github</button> </div>
NB : afin d’alléger l’article, toutes les templates ont été simplifiés. Voir le codesur github pour les templates complets.
Voyons ce qui se passe ici en détail.
Premièrement, j’utilise la nouvelle syntaxe de controller connue comme ‘controller as’ syntaxe, apparue avec AngularJS 1.2.x. La première méthode du contrôleur est invoquée lorsque l’utilisateur clique sur le bouton « Login with Github ». Cette méthode délègue l’authentification à $firebaseAuth
qui sera appelée avec le nom du provider d’authentification "github"
. Ensuite, j’observe l’événement "user:logout"
pour permettre à l’utilisateur de se déconnecter (cette événement est émis dans headerCtrl.js).
Et c’est tout ! Le reste sera géré par $firebaseAuth
.
Il ne nous reste plus qu’à créer une nouvelle application sur github (suivez les indications ici) et l’enregistrer dans la Forge de Firebase :
L’authentification de notre nouvelle application est à présent terminée et entièrement fonctionnelle !
Si vous souhaitez tester la synchronisation avec plusieurs utilisateurs différents (et vous n’avez pas des différents comptes github, il peut être utile d’autoriser un autre provider: Twitter, Facebook ou Personna.
Synchronisation des données
AngularFire fournit deux approches pour synchroniser les données : explicite et implicite.
Dans le cas de l’approche explicite, les modifications faites sur le modèle local ne seront pas automatiquement synchronisées, il faudra explicitement mettre à jour les données distantes. Il faut noter que tous les changements des données distantes vont automatiquement apparaître dans le modèle local, ce qui correspond à une synchronisation dans un seul sens. Cette approche est bien adaptée aux situations où vous voulez avoir le contrôle sur la synchronisation des changements locaux vers Firebase.
L’approche implicite quant à elle, permet de synchroniser les données dans les deux sens. Toutes les modifications sur le modèle local seront instantanément envoyées vers Firebase et vice versa. Cette approche est adaptée si vous voulez garder votre modèle local toujours synchronisé avec les données distantes.
Gestion des wishlists
Super. Nous sommes maintenant prêt à implémenter les fonctionnalités de YAWL. Commençons par afficher l’ensemble des wishlists d’un utilisateur connecté.
User story :
- En tant qu’utilisateur je veux pouvoir créer/supprimer des listes d’envies afin de grouper mes envies.
Le premier service a pour but de fournir les références aux données contenues dans notre Firebase :
angular.module('yawl.services.firebaseRefs', ['firebase']) .factory('FireRef', ['$rootScope', 'FBURL', 'Firebase', '$firebase', function ($rootScope, FBURL, Firebase, $firebase) { return { wishlists: function (userId) { userId = userId || $rootScope.auth.user.id; return $firebase(new Firebase(FBURL + '/users/' + userId + '/wishlists')); } } }]);
Pour l’instant, le service FireRef ne contient qu’une seule méthode fournissant la référence vers les wishlists d’un utilisateur. Comme vous pouvez le constater, la méthode wishlists() trace la route vers la structure « wishlists” de l’utilisateur donné :
Par défaut, si l’id de l’utilisateur n’est pas défini, on récupère l’identifiant de l’objet $rootScope.auth.user
. Rappelez-vous, cet attribut géré par $firebaseAuth
contient les informations sur l’utilisateur authentifié. Cela veut dire que nous récupérerons la route vers les wishlists de l’utilisateur courant (connecté). Il est néanmoins possible en spécifiant le userId
de récupérer la route vers les wishlists d’un autre utilisateur. Nous utiliserons cela par la suite.
Notons aussi que la référence standard de Firebase (new Firebase(…)) est encapsulé sans un service $firebase
d’AngularFire. Ce service expose des méthodes utilitaires simplifiant l’utilisation des api Firebase : $child, $add, $remove et autres. Nous allons les utiliser dans notre prochain service.
La route correspondante est déjà en place :
$routeProvider.when('/', {templateUrl: 'partials/wishlists.tpl.html', authRequired: true}); $routeProvider.otherwise({redirectTo: '/'});
Rien de particulier, excepté l’attribut authRequired, il fonctionne exactement comme vous pourriez l’espérer : si l’utilisateur n’est pas encore authentifié via $firebaseAuth
ou son token est expiré, il ne pourra pas accéder à cette route et sera automatiquement redirigé vers la route que vous avez spécifié lors de l’initialisation de $firebaseAuth
(rappelez-vous le paramètre ‘path’).
Il est temps d’écrire un service d’accès aux données :
angular.module('yawl.services.wishlists', ['yawl.services.firebaseRefs']) .factory('wishlistCollection', ['FireRef', function (FireRef) { return { collection: function () { return FireRef.wishlists(); }, find: function (userId, wishlistId) { return FireRef.wishlists(userId).$child('/' + wishlistId); }, create: function (wishlist) { FireRef.wishlists().$add(angular.extend({ creationDate: new Date().getTime() }, wishlist)); }, remove: function (wishlistId) { FireRef.wishlists().$remove(wishlistId); } } }]);
Il y a un certain nombre de choses à expliquer ici. Les méthodes collection()
et find()
ne doivent pas vous surprendre. La première fournit simplement la référence $firebase
vers les wishlists d’un utilisateur connecté, la seconde fournit la référence vers la wishlist donnée en utilisant la méthode utilitaire $child()
. La méthode remove()
permet de supprimer une wishlist donnée à l’aide de $firebase.$remove()
(voir les API de Firebase et AngularFire).
Il est important de comprendre le fonctionnement de la méthode create()
. Quand nous invoquons la méthode $add() sur une référence $firebase, l’objet qui lui est passé en paramètre est inséré à l’endroit indiqué par cette référence. L’id pour cette objet est automatiquement généré par Firebase et ressemblera à quelque chose comme "-J9AEecPWvKnWSlhv4of"
.
Il nous reste à implémenter le contrôleur pour animer le tout :
angular.module('yawl.controllers.wishlists', []). controller('wishlistsCtrl', ['$scope', 'wishlistCollection', function ($scope, wishlistCollection) { this.getWishlistCollection = function () { this.list = wishlistCollection.collection(); }; this.removeWishlist = function (wishlistId) { wishlistCollection.remove(wishlistId); }; this.createWishlist = function (wishlist) { wishlistCollection.create(wishlist); }; }]);
Pour afficher l’ensemble des wishlists d’un utilisateur, nous utilisons la synchronisation explicite dans la méthode getWishlistCollection()
: il suffit juste d’assigner la référence $firebase à une variable list
dans notre scope. Cette variable contiendra la collection des wishlists une fois qu’elle sera récupérée depuis FireBase. Alors, avec tout ça nous pouvons maintenant créer, lire, mettre à jour et supprimer les wishlists.
Le template :
<div ng-controller="wishlistsCtrl as wishlists" ng-init="wishlists.getWishlistCollection()"> <div ng-repeat="(wishlistId, wl) in wishlists.list"> <a href="#/wl/{{ user.id }}/{{ wishlistId }}"> <h3>{{ wl.name }} - {{ wl.creationDate | date }}</h3> <p>{{ wl.description }}</p> </a> <button ng-click="wishlists.removeWishlist(wishlistId)">Remove</button> </div> <form ng-init="wishlist={}">données initiales <input ng-model="wishlist.name" placeholder="Wishlist Name" required/> <textarea ng-model="wishlist.description" placeholder="Description"></textarea> <button ng-click="wishlists.createWishlist(wishlist)">Add</button> </form> </div>
Alléluia ! Les wishlists de l’utilisateur sont maintenant synchronisées avec Firebase… et avec tous les clients connectés ! Essayez de vous connecter avec le même compte sur plusieurs fenêtres de navigateur et ajouter / supprimer une wishlist.
Articles d’une wishlist
Nous continuons d’implémenter les fonctionnalités de YAWL.
User stories :
- En tant qu’utilisateurje veux pouvoir ajouter/supprimer les articles dans mes listes d’envies afin de constituer une liste des cadeaux
- En tant qu ‘utilisateur invitée je veux pouvoir accéder aux listes des autres utilisateurs par un lien afin de voir leurs articles
- En tant qu‘utilisateur invitée je veux réserver/relâcher un ou plusieurs articles afin de montrer mon intention de cadeau
- En tant qu‘utilisateur je veux pouvoir générer un lien vers chaque liste d’envies afin de le partager avec mes invités.
Comme tout à l’heure, ajoutez une méthode dans FireRef qui fournira une référence vers les articles stockés dans Firebase.
... items: function (wishlistId, userId) { return this.wishlists(userId).$child(wishlistId).$child("items"); } ...
Notons ici, que la méthode wishlists()
du même service est utilisée pour récupérer la référence vers les wishlists d’un utilisateur.
Chaque référence $firebase
a une fonction $child()
qui prend en paramètre le chemin vers un enfant et retourne une référence pointant sur cet enfant. Nous avons alors le chemin suivant :
Par ailleurs, il aurait été possible de créer le même chemin de façon suivante : return this.wishlists(userId).$child(wishlistId + “/items")
;
Contrôleur :
angular.module('yawl.controllers.wishlist', []). controller('wishlistCtrl', ['$scope', '$routeParams', '$location', 'wishlistCollection', 'FireRef', '$firebase', function ($scope, $routeParams, $location, wishlistCollection, FireRef, $firebase) { $scope.$locationUrl = $location.absUrl(); $scope.ownerId = $routeParams["ownerId"]; var wishlistId = $routeParams["wishlistId"]; var wishlist = wishlistCollection.find($scope.ownerId, wishlistId); wishlist.$bind($scope, 'wishlist'); $scope.addNewItem = function (item) { if (!$scope.wishlist.items) $scope.wishlist.items = {}; $scope.wishlist.items[new Date().getTime()] = angular.extend({reserved: ""}, item); }; $scope.removeItem = function (itemId) { delete $scope.wishlist.items[itemId]; }; $scope.reserveItem = function (itemId) { $scope.wishlist.items[itemId].reserved = $scope.auth.user.id; }; $scope.releaseItem = function (itemId) { $scope.wishlist.items[itemId].reserved = ""; }; $scope.isReservedByMe = function (item) { return $scope.auth.user && item.reserved == $scope.auth.user.id; }; return $scope; }]);
Ceci mérite quelques explications. Dans ce contrôleur, nous utilisons la synchronisation implicite avec la méthode $bind
appelée sur un objet retourné par $firebase
. Cette méthode prend deux paramètres :
- l’objet $scope ;
- le nom du modèle stringifié.
Elle retourne une promesse qui sera résolue quand nous recevrons les données depuis le serveur FireBase. Une fois bindées, les données seront mises à jour en temps réel, et nous pouvons toujours supposer que les données affichées à l’utilisateur seront à jour.
Dans le reste du contrôleur, il n’y a rien de particulier, nous manipulons les données comme si elles étaient juste des modèles locaux : les méthodes addNewItem()
et removeItem()
permettent d’ajouter / supprimer les items dans une wishlist, reserveItem()
et releaseItem()
permettent de réserver et relâcher une réservation. Quant à la méthode isReservedByMe()
, elle retourne true
si l’article a été réservé par l’utilisateur connecté.
Le template correspondant :
<div class="wishlist" ng-controller="wishlistCtrl as wl"> <h2>{{ wl.wishlist.name }}</h2> <p>{{ wl.wishlist.description }}</p> <input disabled ng-model="wl.$locationUrl"/> <div ng-repeat="(itemId, item) in wl.wishlist.items"> <h3> <a ng-href="{{ item.url }}" target="_blank">{{ item.name }} <i class="fa fa-star" ng-show="isReservedByMe(item)"></i> </a> <span ng-show="item.price">{{ item.price }}€</span> </h3> <p>{{ item.description }}</p> <button ng-click="wl.reserveItem(itemId)" ng-hide="item.reserved">Reserve</button> <button ng-click="wl.releaseItem(itemId)" ng-show="item.reserved" ng-disabled="!isReservedByMe(item)">Release</button> <button ng-click="wl.removeItem(itemId)" ng-show="wl.ownerId == auth.user.id">Remove</button> </div> <div ng-init="newItem={}” ng-show="wl.ownerId == auth.user.id"> Add new item: <input ng-model="newItem.name" placeholder="Title"> <input ng-model="newItem.url" placeholder="Url"> <input type="number" ng-model="newItem.price" placeholder="Price"> <textarea ng-model="newItem.description" placeholder="Description"></textarea> <button ng-click="wl.addNewItem(newItem);">Add</button> </div> </div>
Nous affichons le nom, la description et l’ensemble des articles (items) d’une wishlist. En fonction de la réservation, ou non, d’un article, nous montrons respectivement le boutons “Reserve” ou le bouton “Release”. Ces derniers deviennent grisés si l’article est réservé par un autre utilisateur !isReservedByMe(item)
. Le bouton “Remove” n’est affiché que si l’utilisateur connecté est le propriétaire de la wishlist, de même pour le formulaire d’ajout de nouvel article. Pour finir, une étoile (fa-star) s’affiche si l’item a été réservée par l’utilisateur connecté.
Il ne faut évidemment pas oublier la route vers cette vue :
$routeProvider.when('/wl/:ownerId/:wishlistId', {templateUrl: 'partials/wishlist.tpl.html', authRequired: true});
Notre application est maintenant entièrement fonctionnelle.
Oui, mais ce n’est pas encore fini. Il nous reste une partie très importante : la sécurité.
Sécurité
User stories
- En tant qu‘utilisateur je veux que mes listes d’envies ne soient modifiables que par moi afin de protéger mes données.
- En tant qu‘utilisateur je veux que mes invités ne puissent pas réserver/relâcher les articles qui ont été déjà réservés par les autres utilisateurs afin d‘éviter de recevoir 2 fois le même cadeau.
Il est évident que cacher le bouton “Remove” ne peut pas prétendre être une solution de sécurité robuste. L’utilisateur peut altérer le code javascript de l’application ou juste appeler la fonction correspondante depuis la console de son navigateur.
Firebase fournit une solution très élégante pour résoudre les problèmes de sécurité. Cette solution est basée sur des règles permettant de décrire comment vos données peuvent être lues et écrites. Ces règles demeurent sur les serveurs Firebase et sont automatiquement vérifiées en temps réel pour tout accès aux données.
Les règles sont écrites dans un objet JSON qui correspond à la structure des données dans Firebase. Ces règles limitent l’accès et valident le schéma des vos données.
Il y a trois types de règles que l’on peut utiliser : .read, .write et .validate. Les read & write sont utilisés pour limiter l’accès respectivement à la lecture et à l’écriture (!). Le validate est généralement utilisé pour valider la structure de données une fois que l’accès en écriture est accordée.
Il y a un aspect très important à comprendre. Les règles read et write donnent l’accès à un emplacement des données en incluant tous ses fils, indépendamment des autres read et writes qui peuvent être présentes. Alors que pour le validate, c’est le contraire : tous les validates doivent passer avant que l’écriture soit autorisée.
Firebase fournit l’accès à un certain nombre de variables que l’on peut utiliser lors de la définition des règles:
- ‘auth’ correspond à l’utilisateur connecté ;
- ‘data’ est un snapshot des données actuelles ;
- ‘newData’ est un snapshot des données qui sont en train d’être écrites.
Comme je l’ai déjà mentionné, la structure des règles reflète la structure des données. Cela fonctionne bien, mais il faut connaître les noms exacts de tous les noeuds jusqu’à un emplacement particulier. Dans le cas de YAWL, l’identifiant de l’utilisateur est dynamique et ne peut pas être connu à l’avance (idem pour les identifiants des wishlists et articles). Vous pouvez alors utiliser une variable préfixé avec un symbole “$” définissant l’emplacement qui sera utilisé pour tous les enfants pour lesquels les règles ne sont pas spécifiées explicitement, par exemple $user
(tout ce qui suit le « $ » n’est pas important). Cette variable pourrait ensuite être référencée dans vos règles.
Afin de mieux comprendre, je vous conseille de lire https://www.firebase.com/docs/security/security-rules.html.
Pour spécifier les règles de sécurité de votre Firebase, allez dans l’onglet “Security” de la Firebase Forge.
Voici les règles pour YAWL :
{ "rules": { ".read": "auth != null", "users" : { "$user" : { "wishlists" : { ".write": "auth != null && auth.id == $user", "$wishlist" : { ".validate": "newData.hasChildren(['name'])", "name" : { ".validate": "newData.val() != ''" }, "items" : { "$item" : { ".validate": "newData.hasChildren(['name'])", "name" : { ".validate": "newData.val() != ''" }, "reserved" : { ".write": "auth != null && (data.val() == '' || data.val() == auth.id)", } } } } } } } } }
Tout d’abord, tous les utilisateurs authentifiés peuvent lire l’ensemble des données : ".read": "auth != null"
. Le noeud $user
correspondra à l’id de n’importe quel utilisateur et sera accessible par la suite dans une variable $user
. Avec la règle ".write": "auth != null && auth.id == $user”
on autorise l’accès à l’écriture des wishlists pour les utilisateurs authentifiés et dont l’identifiant (auth.id) correspond au nom du noeud, c’est à dire au propriétaire de wishlist.
Et la dernière règle pour l’écriture ".write": "auth != null && (data.val() == '' || data.val() == auth.id)"
autorise à un utilisateur tiers de changer la valeur de l’attribut reserved
d’un item ($item) dans n’importe quelle wishlist ($wishlist), et cela seulement dans le cas où cette valeur n’est pas encore renseignée ou est égale à l’id de l’utilisateur connecté. Cela correspond à une volonté de permettre à n’importe quel utilisateur de réserver un article si il n’a pas encore été réservé et d’annuler la réservation seulement s’il en est à l’origine.
Les règles .validate sont là pour valider le schéma en imposant d’avoir au moins un attribut name
non vide pour chaque objet wishlist
et item
.
Il y a une subtilité importante à comprendre. En l’état, la réservation par un autre utilisateur ne fonctionne pas ! AngularFire ne permet pas l’accès au noeud reserved
et renvoie l’erreur suivante :
FIREBASE WARNING: set at /users/107448528/wishlists/-J8aw25D6jQdiWtLpMRa failed: permission_denied
La cause est dans l’implémentation des méthodes suivantes :
$scope.reserveItem = function (itemId) { $scope.wishlist.items[itemId].reserved = $scope.auth.user.id; }; $scope.releaseItem = function (itemId) { $scope.wishlist.items[itemId].reserved = ""; };
En effet, nous essayons d’écrire dans le chemin /users/$user/wishlists/$wishlist
ce qui n’est pas autorisé à un utilisateur tiers (seul le propriétaire en a le droit). Pour corriger cela, modifions les méthodes de façon à écrire à l’emplacement exact de l’attribut reserved
(/users/$user/wishlists/$wishlist/items/$item/reserved
) :
$scope.reserveItem = function (itemId) { var itemToReserve = FireRef.items(wishlistId, $scope.ownerId).$child(itemId).$child("reserved"); itemToReserve.$set($scope.auth.user.id); }; $scope.releaseItem = function (itemId) { var itemToReserve = FireRef.items(wishlistId, $scope.ownerId).$child(itemId).$child("reserved"); itemToReserve.$set(""); };
Et cela fonctionne à merveille maintenant. Notre application est terminée et entièrement fonctionnelle !
Démo
Vous pouvez voir le code source de l’application finie ici et une démo là.
Identifiez-vous avec des comptes différentes pour mieux comprendre le fonctionnement. Il vous faudra ouvrir deux navigateurs différents ou deux fenêtres incognito sous chrome (à cause de l’authentification automatique).
Conclusion
A ce point, nous avons fait le tour des principaux concepts de Firebase, angularFire et $firebaseAuth, qui peuvent vous servir de base pour créer vos propres applications avec Firebase et AngularJS.
Les plans tarifaires sont accessibles ici. Les prix varient en fonction de la quantité de données que vous comptez transférer, l’espace de stockage et le nombre de connexions simultanées. Le plan de développement gratuit donne accès à 100Mo de stockage et 5Go de trafic, ce qui est certainement suffisant pour tester et même pour faire des petits projets perso.
Pour finir, je tiens à mentionner la question qui est souvent posée par les développeurs venant du monde SQL : « Comment je cherche et interroge mes données ? ».
Il y a un excellent blog post sur ce sujet écrit par un des créateurs de Firebase. En deux mots – la solution est la dénormalisation.
Firebase a deux moyens pour requêter les données : par chemin et par priorité. Vous ne pourrez jamais faire de requêtes avec des jointures sur n’importe quel champ, une recherche partielle, etc… Mais cela n’était pas un objectif.
Firebase a été conçue pour la performance et la scalabilité, pour servir un nombre virtuellement infini de clients sans dégrader ses performances, ce qui est évidemment impossible avec les bases relationnelles.