Ember.js ,Think different ! (Etape 5 - BO JHipster)

On associe souvent JHipster et Angular. Pourtant JHipster est avant tout une redouble usine permettant une mise en place rapide d’un backoffice métier incluant tout l’attirail de la persistance et de la communication avec les différentes ressources de l’entreprise.

Dans ce post, nous montrerons qu’Ember sait également porter la moustache et être utilisé au dessus d’un développement JHipster.

Cet article fait partie d’une série composée des posts suivants :

  • Introduction à Ember – Rappel rapide sur les mécanismes mis en place par ce framework,
  • Etape 1  – Application simple avec données ‘rack’ retournées directement par le modèle sous forme d’un tableau JSON. Cette étape sera complété par l’utilisation de addons Ember pour rendre l’application plus esthétique et ergonomique.
  • Etape 2 (ce post) –  Passage sur un modèle géré par Ember Data. Etant donné qu’aucun back-office ne sera déployé, nous introduirons le framework de mock Mirage pour intercepter les appels HTTP.
  • Etape 3 – Introduction d’une relation rack —> bottle, et enrichissement des données mockées.
  • Etape 4 –  Construction d’un composant graphique permettant une représentation d’un rack et simplifiant les templates d’affichage
  • Etape 5 (ce post) – Mise en place d’un backoffice construit à l’aide de JHipster. Nous profiterons des capacités de cet outil pour obtenir rapidement un modèle persisté en base, les services Rest permettant de l’administrer ainsi qu’une interface d’administration. Nous verrons au passage quelques problématiques liées à la sécurité.
  • Etape 6 – Mise en place d’un backoffice sur services Liferay et packaging de l’application Ember au sein d’une portlet. Nous avons choisi un backoffice Liferay pour sa facilité à construire un modèle de persistance mais surtout pour bien démontrer l’utilisation potentielle des Adapters et des Serializers, même si un backoffice plus conforme aux standards Rest n’en sera que plus aisé à utiliser.

Le code de cet article est disponible sur mon gitlab dans les branches step_5_x_jhipster.

Création de l’application JHipster

JHipster est une solution de développement applicatif intégrant Spring Boot et Angular de façon simple et efficace et mettant en place une approche de génération de modèle très poussée.

On partira du principe que les outils utilisés par JHipster sont déjà installés (en particulier yo).

On va donc commencer par créer un répertoire jhipster dans le répertoire principal de notre projet, puis une fois placé dans ce répertoire, on exécute la commande :

yo jhipster

On passe alors à la phase d’interrogatoire en donnant les réponses suivantes :

Les points importants sont d’utiliser une application monolithique (pour simplifier les choses, même si l’approche micro-service est réellement pertinente dans notre cas) et de passer par un token JWT.

On injecte ensuite notre modèle par JDL avec le fichier cellar.jh :

entity Rack {
name String,
  nbColumns Integer,
  nbRows Integer,
  image String,
  createdAt LocalDate
}

entity Bottle {
name String,
   createdAt LocalDate,
  yrow Integer,
  xcolumn Integer,
  flipped Boolean
}

relationship OneToMany {
Rack{bottles} toBottle
}

Les entités de notre modèle seront créées dans l’application par la commande :

yo jhipster:import-jdl cellar.jh

On peut répondre avec les choix par défaut à toutes les questions, notre application étant encore en phase de construction.

Reste à faire un petit tour dans MySQL pour aller créer une base nommée ‘jhipstercellar’ et le tour est joué !

La commande ./gradlew va lancer la compilation et démarrer l’application sur le port 8080 :

Pour la suite de ce tutorial, je vous laisse vous loguer avec les identifiants admin / admin et aller dans le menu ‘Entities’ pour créer quelques ‘Racks’ et quelques ‘Bottles’ dans ces casiers.

Dans notre cas cependant, l’interface graphique utilisateur mise en place par JHipster ne nous intéresse pas puisqu’elle repose sur Angular. Ce qui nous intéresse c’est par contre les services REST disponibles sans manipulation supplémentaire. On va les tester à l’aide de Postman.

Pour les utiliser, il faut récupérer au préalable un jeton JWT.

Pour cela, on utilise le service disponible sur ‘api/authenticate’ via une requête POST en lui passant dans le Body un flux JSON (en raw) contenant le compte à utiliser :

Le message retourné contient le token convoité.

Pour appeler les services REST disponibles (et documentés dans le menu ‘Administration / API’), il suffit maintenant d’ajouter dans le Header des requêtes une clef ‘Authorization’ avec pour valeur ‘Bearer <id_token>’ :

C’est cette mécanique qui sera mise en place dans l’application Ember pour se logguer et utiliser les services de l’API JHipster.

L’objectif est maintenant de se placer dans le mode de fonctionnement suivant :

Pour cela, nous allons détailler les modifications à apporter à notre projet Ember.

Step 5a.2 Création d’un adapter / serializer Ember pour JHipster

Installation d’un addon pour la gestion de l’authentification par token JWT

Pour la gestion du token JWT, on va comme souvent s’appuyer sur les add-ons existant dans la communauté Ember (https://www.emberaddons.com/). On installe alors les deux extensions suivantes :

ember install ember-simple-authember install ember-simple-auth-token

Pour finir, il faut également procéder à la configuration du token transporté afin de le mettre en conformité avec ce qu’attend JHipster. Pour cela, on modifie le fichier environment.js du répertoire config pour y ajouter les déclarations suivantes :

ENV['ember-simple-auth'] = {
authorizer: 'authorizer:token'
};
ENV['ember-simple-auth-token'] = {
refreshAccessTokens: true,
refreshLeeway: 300, // Refresh the token 5 minutes (300s) before it expires.
serverTokenEndpoint: '/api/authenticate',
identificationField: 'username',
passwordField: 'password',
tokenPropertyName: 'id_token'
};

Au passage, on y ajoute également la gestion du paramètre de lancement de serveur –jhipster en ajoutant la fonction en tête de fichier :

function usingProxy() {
var usingProxyArg = !!process.argv.filter(function (arg) {
  return arg.indexOf('--proxy') === 0 || arg.indexOf('-pr') === 0 || arg.indexOf('-pxy') === 0;
}).length;

var hasGeneratedProxies = false;
var proxiesDir = process.env.PWD + '/server/proxies';
try {
  fs.lstatSync(proxiesDir);
  hasGeneratedProxies = true;
} catch (e) {}

return usingProxyArg || hasGeneratedProxies;
}

function usingJhipster() {
let isJhipster = !!process.argv.filter(function (arg) {
  return arg.indexOf('--jhipster') === 0 || arg.indexOf('-jhi') === 0
}).length;
return isJhipster;
}

Et en complétant la variable APP :

APP: {
  // Here you can pass flags/options to your application instance
  // when it is created
  proxy: usingProxy(),
  jhipster: usingJhipster()
}
};

Notre application est maintenant prête à gérer un jeton JWT et à travailler avec un backoffice JHipster !

Mise en place d’une page de login pour JHipster

Pour gérer cette authentification, on ajoute une page de login, spécifiquement pour le mode JHipster. On va donc procéder à 3 modifications :

  1. L’ajout d’une route correspondant à la page de login, en modifiant le fichier routeur.js
import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});

Router.map(function() {
this.route('racks');
this.route('rack', { path: '/rack/:rack_id' });
if (config.APP.jhipster) {
  this.route('login'); 
}
});

export default Router;

On modifie également le comportement de la route de plus haut niveau (application.js) pour intégrer la gestion de l’authentification dans le routage des pages :

// app/routes/application.js
import Ember from 'ember';
import ENV from './../config/environment';

export default Ember.Route.extend({

session: Ember.inject.service(),

beforeModel() {
   if (ENV.APP.jhipster && ENV.APP.proxy && !this.get('session').get('isAuthenticated')) {
     this.transitionTo('login');
   } else {
     this.transitionTo('racks');
   }
 },
});
  1. La création de la page de login. On utilisera une route par défaut (pas de modèle à aller chercher), mais il est nécessaire de créer le template de la page contenant le formulaire de login et le contrôleur en charge du traitement de l’action. On a ainsi :
{{!-- app/templates/login.hbs --}}
<form {{action 'authenticate' on='submit'}}>
<label for="identification">Login</label>
{{input id='identification' placeholder='Enter Login' value=identification}}
<label for="password">Password</label>
{{input id='password' placeholder='Enter Password' type='password' value=password}}
<button type="submit">Login</button>
{{#if errorMessage}}
  <p>{{errorMessage}}</p>
{{/if}}
</form>

et

// app/controller/login.js
import Ember from 'ember';

export default Ember.Controller.extend({
session: Ember.inject.service(),

actions: {
  authenticate: function() {
    var credentials = this.getProperties('identification', 'password'),
      authenticator = 'authenticator:jwt';
    var ctx = this;

    this.get('session').authenticate(authenticator, credentials).then(function () {
      ctx.transitionToRoute('racks');
    });
  }
}
});
  1. L’adaptation du template de l’application pour y intégrer un bouton menant à la page de login et pour permettre la déconnexion et le traitement de l’action de déconnexion dans le contrôleur associé :
{{!-- app/templates/application.hbs --}}

<div class="container">
 <div class="page-header">
   <h1>Ember Cellar</h1>
   <p class="lead">Basic layout using Bootstrap for a simple wine application</p>
   {{#if isJhipster}}
   <p  class="text-right">
     {{#if session.isAuthenticated}}
         <a {{action 'invalidateSession'}}>Logout</a>
      {{else}}
         {{#link-to 'login'}}Login{{/link-to}}
      {{/if}}
   </p>
   {{/if}}
</div>
    {{outlet}}
</div>

et

// app/controller/application.js
import Ember from 'ember';
import ENV from './../config/environment';


export default Ember.Controller.extend({
 session: Ember.inject.service('session'),

isJhipster: ENV.APP.jhipster,

 actions: {
    invalidateSession() {
       this.get('session').invalidate();
       this.transitionToRoute('login');
    }
 }
});

On a maintenant une page de login qui nous permet de récupérer un jeton JWT. Les services Web applicatifs vont pouvoir être interrogés en mode sécurisé.

Reste à adapter la vision d’un service REST de JHipster (assez sommaire…) à celle du connecteur REST d’Ember Data…

Adaption des services REST JHipster

Pour cette opération, on va s’appuyer sur l’adapter REST déjà présent dans Ember Data (DS.RESTAdapter), en l’enrichissant du mixin du composant ember-simple-auth pour la gestion token JWT.

On doit également ‘corriger’ le flux JSON de JHipster pour y intégrer les ‘root names’ des objets. En effet, les Web Services JHipster (type “http://localhost:8080/api/racks”) retournent un flux du type :

[
 {
   "id": 1,
   "name": "Jhipster Rack 1",
   "nbColumns": 6,
   "nbRows": 8,
   "image": "img/rack_01.jpg",
   "createdAt": "2017-05-21"
 },
 {
   "id": 2,
   "name": "JHipster Rack 2",
   "nbColumns": 5,
   "nbRows": 6,
   "image": "img/rack_04.jpg",
   "createdAt": "2017-05-21"
 }
]

Alors que le flux REST attendu par le connecteur d’Ember Data est du type :

{
“Racks”, [
 {
   "id": 1,
   "name": "Jhipster Rack 1",
   "nbColumns": 6,
   "nbRows": 8,
   "image": "img/rack_01.jpg",
   "createdAt": "2017-05-21"
 },
 {
   "id": 2,
   "name": "JHipster Rack 2",
   "nbColumns": 5,
   "nbRows": 6,
   "image": "img/rack_04.jpg",
   "createdAt": "2017-05-21"
 }
]
}

On surcharge donc la méthode handleResponse de DS.RESTAdapter pour aller chercher l’information de type d’objet de façon un peu directive dans l’URL d’appel : 

// app/adapter.js
import DS from 'ember-data';
import ENV from './../config/environment';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';


var JhipsterAdapter = DS.RESTAdapter.extend(DataAdapterMixin, {
   namespace: 'api',
  authorizer: 'authorizer:token',

  handleResponse(status, headers, payload, requestData) {
     var typeKey = requestData.url.slice(this.namespace.length+2, requestData.url.length);
     var pos = typeKey.indexOf("/"); // for response on URL with an Id
     if (pos >= 1) {
         typeKey = typeKey.slice(0,pos-1);
     }
     var newPayload = {};
     newPayload[typeKey] = payload;
     // todo : how to handle embedded relationships...
     return this._super(status, headers, newPayload, requestData);
  },

});

var MirageAdapter = DS.JSONAPIAdapter.extend({
  namespace: 'api',
  shouldBackgroundReloadRecord() {
    return false;
  }
});


var FinalAdapter;
if (ENV.APP.jhipster && ENV.APP.proxy) {
   FinalAdapter = JhipsterAdapter;
} else {
   FinalAdapter = MirageAdapter;
}

export default FinalAdapter;

Il est possible d’aller nettement plus loin dans la customisation de cet adaptateur pour notamment mieux gérer le chargement des ressources liées. Mais cela serait l’objet d’une nouvelle série d’articles !

Il reste encore une dernière chose à faire pour faire correspondre la gestion de la relation 1..n entre Rack et Bottles telle que transportée par le flux JSON de JHipster avec celle attendue par Ember Data. En effet, si le flux retourné pour récupérer l’ensemble des Racks ne contient aucune référence aux bouteilles contenues, celui concernant les bouteilles encapsule totalement le Rack en relation :

{
   "id": 1,
   "name": "Château JHipster",
   "createdAt": "2017-05-21",
   "yrow": 3,
   "xcolumn": 4,
   "flipped": false,
   "rack": {
     "id": 1,
     "name": "Jhipster Rack 1",
     "nbColumns": 6,
     "nbRows": 8,
     "image": "img/rack_01.jpg",
     "createdAt": "2017-05-21"
   }
 }

Pour signaler ce fait à Ember Data (et là on entre de plain-pied dans les subtilités du framework qui font sa difficulté d’apprentissage), il est nécessaire d’ajouter un Serializer spécifiquement à l’objet Bottle (et au mode JHipster) signalant ce fait. Pour cela, on exécute la commande suivante :

ember generate serializer bottle

Et on ajoute les indications suivantes dans le sérialiseur créé :

// app/serializers/bottle.js
import DS from 'ember-data';
import ENV from './../config/environment';

var JhipsterSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin,{
  attrs: {
    rack: { embedded: 'always'}
  }
}
);

var MirageSerializer = DS.JSONAPISerializer.extend({
});

var FinalSerializer;
if (ENV.APP.jhipster && ENV.APP.proxy) {
FinalSerializer = JhipsterSerializer;
} else {
FinalSerializer = MirageSerializer;
}

export default FinalSerializer;

En vérifiant bien que notre application JHipster est toujours démarrée, on peut alors démarrer le serveur Node via la commande :

ember s --proxy http://localhost:8080 --jhipster

On retrouve alors tous les casiers et toutes les bouteilles créés au travers de l’administration JHipster :

A noter qu’un démarrage du serveur node sans proxy (ember s) permet toujours de travailler avec des données mockées par Mirage.