Chapitre 4 appels serveurs

L’objectif principal de cette librairie est d’apporter les fonctionnalités liées aux ordres de paiement de l’application PayZen dans l’application Lyra Collect. Mettre ces écrans dans une librairie nous permet de mutualiser le code et facilite la maintenance.

Les ordres (aussi appelés liens) de paiement, sont des liens générés par le marchand qu’ils peuvent envoyer à leur client pour se faire payer. Ces liens amènent vers la page de paiement PayZen du marchant avec toutes les informations de paiement pré-remplies.

Voici quelques exemples d’écrans pour les ordres de paiement (liste, détail, création) :

Dans les articles précédents, nous avons répondu aux problématiques liées aux dépendances, à l’environnement de développement et aux personnalisations.

Il nous reste un sujet à aborder, qui est spécifique aux écrans des ordres de paiement, ce sont les appels serveurs et l’authentification.

En effet, la librairie expose des composants de haut niveau qui peuvent être des écrans complets :

  • PaymentOrderList : écran complet de la liste des ordres de paiement, avec ses propres appels serveur
  • PaymentOrderDetail : écran complet du détail d’un ordre de paiement, avec ses propres appels serveur
  • PaymentOrderCreation : enchainement d’écrans pour la création d’un ordre de paiement, avec ses propres appels serveur
  • PaymentOrderSettings : section de paramètres des ordres de paiement

L’architecture de nos services

Nos deux applications communiquent avec leur back-end respectif (donc URL différentes) qui ont chacun leur méthode d’authentification spécifique.

  • Côté PayZen, c’est une authentification à base de cookie et de tokens.
  • Côté Lyra Collect, l’authentification s’appuie sur RH-SSO (Keycloak), à base de Bearer Token.

Ce qu’il faut savoir, c’est que la plateforme Lyra Collect se base sur la solution PayZen. Et les ordres de paiement sont une notion portée par PayZen.

Donc pour l’app PayZen, aucun problème avec les ordres de paiement : elle communique avec son back-end, et l’authentification est naturellement gérée.

Par contre pour l’application Lyra Collect, il faut communiquer directement avec le back-end PayZen (sauf à devoir dupliquer l’API des Ordres de Paiement sur Lyra Collect, ce que nous voulons justement éviter).Et bien entendu, il n’est pas souhaitable de gérer plusieurs authentifications sur l’application Lyra Collect (une pour les fonctionnalités des Ordres de Paiement, l’autre pour le reste de l’application).

En revanche, les systèmes d’information Lyra Collect et PayZen communiquent très bien entre eux car ils sont hébergés au sein de la plateforme Lyra.

L’idée est donc d’ajouter une brique de proxy d’API devant le back-end Lyra Collect qui aura les rôles suivant :

  1. authentifier l’utilisateur Lyra Collect avec RH-SSO
  2. transférer l’appel vers PayZen en lui adjoignant les paramètres d’authentification (PayZen) correspondant

Voici un schéma simplifié qui illustre l’architecture cible. En vert l’existant, en orange les nouveaux composants :

La gestion des appels serveurs dans la librairie

Une fois ces modules en place, la problématique suivante est : comment la librairie va pouvoir effectuer ses requêtes alors qu’elle ne connait ni l’URL du serveur ni la méthode d’authentification à configurer ?

En effet, elle est utilisée à la fois dans l’application Lyra Collect (qui communique avec le backend Lyra-Collect) et PayZen (qui communique… oui, avec le backend PayZen).

Et ce que nous voulons éviter, c’est qu’à chaque appel, la librairie fasse quelque chose comme :

Première hypothèse

Au début, nous avons imaginé que les applications passent des paramètres au niveau du Provider de la librairie :

  • baseURL : l’URL de base à utiliser pour tous les appels
  • mode : EP / VAD selon le mode d’authentification à utiliser
  • token : le token d’authentification

Ce qui donnerait :

// depuis l’app PayZen // depuis l’app Lyra Collect

La librairie dispose ainsi que tout le nécessaire pour effectuer les appels. Selon le mode d’authentification, le token sera utilisé différemment.

Après un essai, cela fonctionne mais plusieurs points ne nous conviennent pas :

  • le mode revient à utiliser un if/else que nous voulions éviter. De plus, cela introduit un couplage fort entre la librairie et les applications qui l’utilisent (la librairie doit forcément connaître ces dernières…)
  • le nouveau token PayZen est passé dans le retour de l’appel, il faut donc mettre en place un mécanisme pour retourner le nouveau token à l’appli
  • l’authentification côté PayZen utilise des cookies, et pour que ça fonctionne, il vaut mieux utiliser la même API de requête → nouveau couplage app / lib

(erreur)  Cette hypothèse n’est pas retenue


Deuxième hypothèse

Une solution alternative serait d’utiliser un client HTTP autre que l’API native ‘fetch’. Ainsi avec la librairie Axios, il est possible de :

  • créer une instance de client HTTP avec des paramètres, par exemple une URL de base
  • utiliser des middlewares pre- et post-request, par exemple pour configurer l’authentification

Avec Axios, l’application peut ainsi créer une instance de client HTTP et la configurer avec toutes ses spécificités (URL de base et authentification notamment) et la transmettre à la librairie.

Création de l’instance Axios :

import Axios from 'axios'
 
// création de l'instance Axios
export const axios = Axios.create({
  baseURL: 'https://api.example.com',
})

Configuration d’une authentification Bearer

import { useContext, useLayoutEffect, useRef } from 'react'
import { SessionContext } from '../contexts/SessionContext'
import { axios } from './axios'
 
export function useAuthInterceptor() {
  // on récupère le token depuis un contexte
  const { accessToken } = useContext(SessionContext)
  // stockage de l'identifiant de l'intercepteur dans une référence React
  const reqInterceptor = useRef<number>()
 
  useLayoutEffect(() => {
    // si l'interceptor existe déjà, on le libère
    if (reqInterceptor.current !== undefined) {
      axios.interceptors.request.eject(reqInterceptor.current)
    }
 
    // déclaration d'un nouvel intercepteur de requête, appelé avant chaque requête
    reqInterceptor.current = axios.interceptors.request.use((config) => {
      if (accessToken && config.headers) {
        // configuration du header Authorization de la requête
        config.headers.Authorization = `Bearer ${accessToken}`
      }
 
      // on retourne la config modifiée
      return config
    })
 
    return () => {
      // au nettoyage, on libère l'intercepteur
      if (reqInterceptor.current !== undefined) {
        axios.interceptors.request.eject(reqInterceptor.current)
      }
    }
  }, [accessToken])
}

Déclaration du Provider dans l’app

import { Provider } from '@lyra/shared-components'
import { axios, useAuthInterceptor } from './rest'
import { Root } from './components/Root'
 
export const App = () => {
  // initialisation de notre intercepteur d'authentification
  useAuthInterceptor()
 
  return (
    <Provider axios={axios}>
      <Root />
    </Provider>
   )
}

Avec cette solution, la librairie ne s’occupe plus de gérer :

  • l’URL de base, elle fait seulement des appels relatifs
  • l’authentification, elle est configurée côté application

L’avantage c’est que quelque soit la méthode d’authentification, le fonctionnement de la librairie n’est pas impacté : pas de if / else, pas de mécanisme de synchro de token entre lib et app. Avec les interceptors, chaque application reste maître de sa gestion de token.

Exemple d’appel REST depuis la librairie

import { useContext } from 'react'
import { ConfigContext } from '../contexts/ConfigContext'
 
export const FooComponent = () => {
  const { axios } = useContext(ConfigContext)
  const result = axios.get('/path/to/resource')
  ...
}

En outre, cela nous permet de développer une nouvelle application mobile qui utiliserait la librairie, sans modifier cette dernière.

 ✅ Cette hypothèse répond à nos besoins, elle est donc retenue

Spécifications OpenAPI

Le back-end PayZen fournit des spécifications OpenAPI pour tous ses web services REST. Par conséquent, pour éviter toute erreur de communication avec le back-end, nous utilisons ces contrats pour nous assurer que nos appels sont corrects dès la librairie.

Pour cela, nous utilisons le projet OpenAPI Generator qui permet de générer un client REST en fonction d’un contrat OpenAPI.

Et par chance (ou pas (sourire)), il existe un générateur typescript-axios. Parfait pour notre cas d’utilisation !

Il suffit donc de passer notre instance Axios au client REST généré par typescript-axios, et ce dernier expose les différentes requêtes disponibles sur notre back-end.

Et bien entendu, le client généré contient le typage de tout le modèle.

Voici le fichier de configuration utilisé pour le générateur typescript-axios :

openapi-generator.config.json


{
  "withSeparateModelsAndApi": true,
  "apiPackage": "api",
  "modelPackage": "model",
  "useSingleRequestParameter": true,
  "enumPropertyNaming": "UPPERCASE",
  "stringEnums": true
}

La ligne de commande pour générer le client REST :

Commande pour générer le client REST

npx @openapitools/openapi-generator-cli generate -g typescript-axios -c openapi-generator.config.json -i payment-order.yaml -o src/openapi/payment-order

Dans le code, on peut maintenant utiliser le client généré (PaymentOrderApi dans notre cas) en lui passant l’instance Axios :

Exemple d’un composant utilisant le client REST généré

import { useContext } from 'react'
import { ConfigContext } from '../contexts/ConfigContext'
import { PaymentOrderApi, Configuration } from '../openapi/payment-order'
 
export const PaymentOrderList = () => {
  const { axios } = useContext(ConfigContext)
 
  // récupération du basePath depuis l'instance axios
  const basePath = axios.defaults.baseURL
 
  // création d'une configuration et d'un client d'API avec les objets générés
  const configuration = new Configuration({ basePath })
  const paymentOrderApi = new PaymentOrderApi(configuration, basePath, axios)
 
  // on peut maintenant utiliser notre API qui va effectuer les requêtes en utilisant l'instance axios passée en config
  const result = paymentOrderApi.findPaymentOrder({
    page: 1,
    perPage: 10,
    minAmount: 42,
  })
 
  ...
}

Conclusion

(coche)  La librairie est capable d’effectuer des appels serveurs avec des urls relatives.

(coche)  La gestion des tokens d’authentification et de l’URL du back-end reste de la responsabilité des applications, la librairie ne s’en occupe pas.

(coche)  Le déploiement de la brique “API Proxy” côté Lyra Collect permet d’accéder aux API de PayZen depuis l’app Lyra Collect.

(coche)  Les appels REST sont validés par les specs OpenAPI fournies par le back-end.

Les problématiques liées aux appels serveurs sont résolues. La librairie est donc capable d’embarquer des composants métiers de haut niveau (écrans complets).