Les Frameworks JS dits “Progressifs”

Open Dossard en quelques mots

Open Dossard est une application que nous avons développée pour présenter la richesse et le niveau de productivité d’un atelier de développement constitué des briques suivantes :

  • NestJS : Framework NodeJS dit « Progressif »
  • TypeORM : Mapping Objet/relationnel
  • Swagger : Documentation des APIs et génération de SDK
  • Docker : Pour la conteneurisation des environnements de développement et production

C’est une vitrine technologique en quelque sorte constituée d’un Front en ReactJS et d’un BackEnd en NodeJS/NestJS. Le code source est disponible sur github, n’hésitez pas à le modifier ou l’adapter pour vos besoins.

Le code source complet : https://github.com/dngconsulting/OpenDossard/

L’URL du site institutionnel décrivant les fonctionnalités d’Open Dossard : https://www.opendossard.com

L’URL de l’application (il n’existe pas encore d’accès libre en démonstration) : https://app.opendossard.com

L’API : https://app.opendossard.com/api

La suite de cet article aborde plus en détail les aspects techniques d’Open Dossard. Notez que ce projet a été mené en quelques jours (90% du code source a été réalisé en 2 semaines), il n’est pas parfait mais correspond dans les grandes lignes à notre philosophie du développement NodeJS dit « Progressif ».

La genèse du projet

Comme dans toute société spécialisée dans les services autour du développement logiciel, il nous arrive d’avoir des périodes appelées « période d’inter-contrats ». Les développeurs connaissent bien ce moment durant lequel on leur demande d’écrire (ou réécrire pour la n-ième fois) le logiciel de gestion des compétences internes ou l’outil de gestion interne.

Dans notre cas, nous étions un petit groupe de quatre avec pour certains pas plus de 10 jours de disponibilité. J’aimerai au passage vraiment remercier Guillaume, Haoyu et Jérôme pour m’avoir suivi dans ce projet.

Depuis le temps que nous réalisons des logiciels pour nos clients dans des technologies parfois imposées, il était temps de mettre à profit ce temps « libre » pour quelque chose qui nous tient tous à cœur : réaliser à partir de zéro une application sur laquelle nous pourrions monter en compétence de manière technique et fonctionnelle.

Alors, disons le tout de suite, le métier n’est pas le leitmotiv principal du projet, nous sommes avant tout des passionnés de technologie et notamment de technologies logicielles. Le métier reste l’apanage de nos clients. Mais pour mettre en valeur une technologie, il faut un projet fonctionnellement intéressant. D’autant plus si cela touche quelques-uns de nos arcs sensibles. Après une courte réflexion, le sujet était tout trouvé, le thème qui nous réunit tous les quatre : le sport.

Il existe aujourd’hui de nombreux évènements sportifs pour lesquels les organisateurs sont encore réduits à gérer l’engagement des participants et les résultats avec un tableur Excel ou même un crayon.

L’idée d’Open Dossard est simple : faciliter le travail des organisateurs de courses, cyclistes dans la version actuelle mais qui pourrait être facilement adaptée à d’autres sports (course à pied, trail, aviron, etc…).

Une fois les participants inscrits, l’application leur attribue un numéro de dossard, une catégorie (féminines, cadets, séniors, élites, amateurs, vétéran, …) et les classe à partir de leur numéro de dossard en fin d’épreuve. Une API permettrait de partager à des tiers les performances (classements) des sportifs tout au long de l’année à des fins statistiques (progression, audience, …). Les organisateurs auraient, eux, une sorte de console d’administration leur permettant d’effectuer l’enregistrement des participants (l’engagement) et saisir les résultats post épreuve.

Voilà pour le principe. Une fois l’idée retenue il nous fallait trouver un nom, une identité visuelle, planifier les fonctionnalités prioritaires et surtout trouver l’architecture technique de nos rêves.

La plupart d’entre nous avions déjà une grosse expérience de projets classiques en Java (.NET également) avec un FrontEnd JS (Angular ou React) et un Backend Java (Micro Services, avec et sans JHipster). Nous sommes ce qu’on pourrait appeler vulgairement des « développeurs Full Stack … à l’ancienne ».  Guillaume et moi, compte tenu de notre âge, avons connu les débuts de Java, .NET et PHP. L’ascension phénoménale de JavaScript est quelque chose, en tout cas pour ma part, que j’ai vécu avec un peu de méfiance, notamment après avoir expérimenté les Frameworks objets typés comme GWT qui réunifiait à merveille FrontEnd et BackEnd sous un seul et même langage : Java.

Sortant d’un projet avec du React Native typé flow et un backend Spring Boot généré avec JHipster, on ne s’imagine pas à quel point coder full stack relève de la schizophrénie lorsqu’on passe du Front au Back. On met des points-virgules puis on les enlève, on bricole des lambdas et des fonctions pour adopter côté serveur une attitude fortement typée avec de l’héritage, des classes. On met des ‘->’ sur le serveur puis des ‘=>’ sur le client sans compter les innombrables différences de syntaxe (Stream Java au lieu des collections JS, etc).

Bref, autant il est légitime à l’heure des micro-services de mixer les langages et les technologies, autant nous avons tendance parfois à complexifier les plateformes là où une unification pourrait être possible.

Pour Open Dossard (vous l’aurez donc deviné, le nom de ce petit projet) les choses allaient être différentes. Nous étions décidés à trouver la pile technologique qui allait nous permettre d’aller vite, de coder proprement et surtout de nous faire plaisir en utilisant des concepts maîtrisés et éprouvés. Voilà pour la genèse du projet.

Framework Progressif et Architecture d’entreprise

Il faut avouer que le monde JS a énormément évolué ces dernières années. TypeScript a changé totalement la donne en apportant le typage qui manquait cruellement à JS. TypeScript joue aujourd’hui un peu le rôle du Java d’antan, on peut le dire.

Dans notre quête à l’architecture « idéale » l’ingrédient de base était donc trouvé, TypeScript serait notre allié. Mais pour avoir un environnement totalement unifié, il nous fallait aussi pour le coup NodeJS côté serveur en mode typé. Jusque-là, rien d’insurmontable, bien au contraire, ce type de projet est légion. En revanche, à mesure qu’on avançait côté backend, les choses devenaient de moins en moins claires. Il existe une infinité de Framework JS (même typés) pour réaliser une API en JS : ExpressJS ? Fastify ? Koa ? Hapi ? Restify  ou même Strong Loop d’IBM ? Pourquoi en choisir un plutôt qu’un autre ? Comment vérifier leur maturité ou leurs performances ? ExpressJS est le plus connu mais cela va t-il durer avec la forte progression de Fastity ?

Lorsqu’on vient du monde Java (ou .NET), il y a des fondamentaux sur lesquels on aime se raccrocher. Réécrire un n-ième framework de mapping O/R, gérer la sécurité et les transactions à la main ne fait plus rêver personne en 2020. Ecrire des kilomètres de fonctions aux paramètres variables sans aucun type, encore moins.

En réalité, inconsciemment on cherchait l’équivalent d’un Spring Framework, mais un Spring à la mode JS, c’est-à-dire outillé comme JS, plus performant que les horribles Class Loaders du monde Java avec du live reload façon JS. En quelque sorte, une architecture d’entreprise (au sens « Enterprise Architecture ») multicouches avec des services techniques à base d’annotations à la sauce …JavaScript.

Nos recherches pouvaient donc être plus ciblées. Et elles vont très vite se porter sur l’univers encore bien (trop) méconnu des Framework dits « Progressifs » de NodeJS. Très vite, nous allons découvrir les pépites montantes du domaine. Non seulement ces Framework existent mais ils sont à la mode (!).

Voici la définition donnée par Kamil Mysliwiec, l’auteur charismatique de Nest, qui résume parfaitement bien la philosophie du Framework. « In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like Angular, React and Vue which improve developer productivity and enable the construction of fast, testable, extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers and tools for Node, none of them effectively solve the main problem – the architecture. Nest aims to provide an application architecture out of the box which allows for the effortless creation of highly testable, scalable, loosely coupled and easily maintainable applicationsNest (NestJS) is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with and fully supports TypeScript and combines elements of OOP (Object Oriented Programming) … »

Tout y est remarquablement bien décrit. Son concurrent, Ts.ED a été celui sur lequel notre attention s’est porté en premier « Ts.ED is a framework on top of Express to write your application with TypeScript (or in ES6). It provides a lot of decorators to write your code » . La première version d’Open Dossard a été écrite en Ts.ED puis réécrite avec NestJS (plus complet et plus mature). La migration ne nous a coûté que quelques annotations.

Que ce soit Nest ou Ts.ED, il y avait là tous les ingrédients pour se faire plaisir. Du Swagger généré à partir des décorateurs (les annotations), l’injection de dépendance, la possibilité de créer des services utilisés par des contrôleurs, mais aussi et surtout la notion de plugins ou « Recipe ». Un peu comme Spring et ses modules.

Que ce soit Nest ou Ts.ED, ils fonctionnent avec des connecteurs et charge ensuite à l’utilisateur d’y greffer un outil de mapping objet/relationnel (TypeORM, Sequelize, Magoose, …), un gestionnaire de sécurité (PassportJS par exemple) ou n’importe quel autre service technique …

Après une première période d’implémentation à base de NestJS, il était clair que pour nous la pile technique qui correspondait le mieux à nos attentes était celle-ci.

Unification du langage -> unification de l’écosystème

On a beau dire, le langage de programmation, c’est la base. TypeScript est un langage très agréable à utiliser, nous n’allons pas faire ici son éloge, de nombreux articles pullulent sur le Web. Lorsqu’on vient du monde Java, TS apporte une verbosité moindre avec des sucres syntaxiques comme la destructuration ou les objets littéraux (la possibilité d’initialiser un objet simplement via un flux JSON) qui nous font gagner un temps précieux. Des outils tels que class-transformer pourront aider à créer des DTO ou convertir des types littéraux en classes. Cet article énonce les avantages de TypeScript par rapport à Java, d’un point de vue purement syntaxique.

Nous avons choisi sur Open Dossard de coder le Front en ReactJS typé, c’est une option, Angular aurait également pu être un excellent candidat.

Les avantages d’unifier le langage front et back sont nombreux. Au-delà même du partage des types ou des fonctions utilitaires (collections, dates, …) c’est tout l’environnement de développement qui est semblable : la gestion du packaging et des versions avec npm ou yarn, le mode développement avec le rechargement automatique du client et du serveur, la centralisation des logs, notamment si vous travaillez en même temps sur l’API et un écran du Front. Docker, au travers de docker-compose nous ont permis d’unifier l’expérience développeur (docker-compose logs -f renvoyant à la console courante à la fois les logs des conteneurs Backend et Frontend).

Le tout sans oublier TSLint qui donne la possibilité d’homogénéiser le code source et de mettre en place de bonnes pratiques de codage généralisées au Front et au Back.

L’outillage : Docker pour le développement et la production

Nous avons fait le choix dans Open Dossard d’adopter Docker pour conteneuriser notre environnement de développement. Dans l’équipe, certains codent sous MacOSX, d’autres sous Windows 10. Docker permet de gommer les différences en imposant une seule plateforme de développement, qui sera au passage proche de la production. L’autre avantage de docker est d’assouplir tout changement dans le modèle de déploiement. A titre d’exemple, Open Dossard est proposé en développement sous la forme de deux services, l’un pour le Front ReactJS (port 3000) et l’autre pour l’API et les services (le Backend sur le port 9090). Ces deux environnements communiquant via un simple proxy en développement. En production, nous avons choisi, pour des soucis de simplification, de faire héberger le Front ReactJS et l’API sur le même conteneur. Les deux environnements ont donc été fusionnés, permettant ainsi de centraliser les logs et le monitoring. Ce changement opérationnel ne nous a “coûté” qu’un fichier yaml sous docker-compose. Installer un nouvel environnement de développement (Front & Back & Base de données) se résume à un “git checkout” suivi d’un “docker-compose up”, difficile de faire plus simple.

Le scaffolding du projet

La structure projet respecte un découpage classique type micro service incluant le Front comme un service. Par « service » on entend ici « service » au sens Docker, un conteneur. Il y a donc un répertoire par service, chaque service représentant un projet Node/NPM indépendant.

L’intérêt d’un tel découpage est de permettre de mutualiser les éventuelles configurations (répertoire config) et d’ajouter plus tard de nouveaux micro services facilement.

Le Full Stack Live Reload, ça déchire 🙂

C’est incontestablement ce qui fait aujourd’hui la force de NodeJS, le « cold start » ou démarrage à froid avec le rechargement complet de l’application. Dans cette petite vidéo de démonstration, nous ajoutons un champ à une table en base de données pour l’afficher dans un tableau ReactJS côté client. Cette opération, pas si anodine que cela, aurait nécessité, sans l’outillage en place, l’écriture d’un script de modification de base de données, le changement des contrats de services et probablement l’adaptation de la requête Fetch côté client pour récupérer le nouveau champ en question et l’afficher dans la table. Avec notre atelier de développement,  juste après l’ajout du champ dans l’entité LicenceEntity  NodeJS redémarre instantanément et la colonne est rajoutée dans la table (voir mode Sychronize à true, équivalent du SchemaUpdate d’Hibernate).

Il reste ensuite à répercuter ce changement de modèle du domaine côté client, ce qui est fait grâce à Swagger CodeGen (script buildSDK dans la vidéo). L’entité étant utilisée dans les contrats de service de manière typée, la re-génération du SDK provoque une compilation qui détecte l’ajout du champ et lance le compilateur TypeScript côté client. La même opération avec la pile Spring aurait probablement nécessité plus de temps lié à l’instrumentation du bytecode des proxies dynamique Hibernate (ou un autre ORM).

Cette facilité avec laquelle le développeur passe du Back au Front est réellement quelque chose de plaisant et qui compte dans la productivité finale.

L’utilisation d’OpenAPI et Swagger

Dès lors qu’il est nécessaire de créer une « API » il faut clairement une documentation et un typage. Lorsqu’on pense typage d’API, forcément le premier mot qui nous vient à l’esprit est « Swagger ». En réalité, nous avons plutôt besoin d’OpenAPI. Or, JavaScript étant par essence non typé, il est très complexe de méta-documenter les services sans avoir recours aux décorateurs et autres subterfuges syntaxiques. De nombreuses bibliothèques ont vu le jour ces dernières années et elles sont aujourd’hui matures.

NestJS propose nativement un module Swagger dédié : https://github.com/nestjs/swagger et accessible à l’adresse suivante pour Open Dossard.

Documenter est une première étape. Mais l’autre fonction clé de Swagger est la génération de SDK. Nous allons pouvoir fournir à des clients tiers hétérogènes la possibilité d’utiliser du code qui sera toujours en phase avec nos services (paramètres, modèle de données, …). Dans la vidéo démontrant la productivité que procure notre atelier nous voyons bien qu’en cas de modification du modèle de données le simple fait de re-générer le SDK permet au client de disposer de la nouvelle méthode via le code suivant (utilise le principe de promesses en JS) :

const fetchCompetitions = async () => {
    const results = await apiCompetitions.getAllCompetitions();
    setData(results);
};

const createRace = async (newRace: RaceCreate) => {
    await apiRaces.create(newRace);
};

L’intérêt de passer par un SDK est multiple :

  • Aucune adhérence directe avec l’outil XHR, ici nous utilisons fetch mais nous pouvons migrer plus tard vers axios ou un outil plus performant sans avoir à modifier les multiples références dans le code
  • Les appels au serveur ne sont pas pollués par des services techniques. Typiquement nous utilisons une authentification JWT nécessitant de faire transiter un jeton dans chaque requête lorsque l’utilisateur est connecté. Avec SwaggerCodeGen il suffit de positionner ce jeton directement dans la classe mère du SDK (celle qui est utilisée par toutes les requêtes) pour que ce mécanisme soit transparent.

Cette page recense toutes les technologies supportées par Swagger CodeGen. Nous utilisons la pile typescript-fetch. A noter qu’il est toujours possible de modifier le code source généré par Swagger CodeGen en redéfinissant les template proposés par défaut (pour insérer des entêtes spécifiques, intercepter les requêtes, faire du retry automatique, …).

Les services & contrôleurs

Les services et contrôleurs REST sont la clé de voûte de NestJS. Ce Framework inspiré d’Angular est riche, nous n’entrerons pas dans le détail des nombreuses fonctionnalités comme l’injection de dépendance ou la possibilité d’utiliser un mécanisme à base de modules pour le découpage fonctionnel. Voici le code du contrôleur des licences qui réalise essentiellement du CRUD mais cela donne tout de même une idée.

import {LicenceEntity} from '../entity/licence.entity';
import {EntityManager, Repository} from 'typeorm';
import {Body, Controller, Delete, Get, Param, Post, Put, UseGuards} from '@nestjs/common';
import {InjectEntityManager, InjectRepository} from '@nestjs/typeorm';
import {ApiOperation, ApiResponse, ApiUseTags} from '@nestjs/swagger';
import {Filter, LicencesPage, Search} from './shared.model';
import {FederationEntity} from '../entity/federation.entity';
import {AuthGuard} from '@nestjs/passport';

/**
 * Licence Controler is in charge of handling rider licences
 * Mainly Crud operations & pagination
 */
@Controller('/api/licences')
@ApiUseTags('LicenceAPI')
@UseGuards(AuthGuard('jwt'))
export class LicenceController {
    constructor(
        @InjectRepository(LicenceEntity)
        private readonly repository: Repository,
        @InjectEntityManager()
        private readonly entityManager: EntityManager,
    ) {
    }

    @Get('/search/:param')
    @ApiOperation({
        operationId: 'getLicencesLike',
        title: 'Recherche des licences',
        description: 'Rechercher des licences en fonction, du nom, prénom ou numéro de licence ',
    })
    @ApiResponse({status: 200, type: LicenceEntity, isArray: true, description: 'Liste des licences'})
    public async getLicencesLike(@Param('param') param: string): Promise {
        const filterParam = '%' + param.replace(/\s+/g, '') + '%';
        const query: string = `SELECT l.* FROM licence l WHERE CONCAT(UPPER(l.name),UPPER(unaccent(l."firstName"))) like $1 
                               LIKE $1 FETCH FIRST 20 ROWS ONLY`;
        return await this.entityManager.query(query, [filterParam]);
    }

    @Get(':id')
    @ApiOperation({
        operationId: 'get',
        title: 'Rechercher une licence par ID ',
        description: 'Recherche une licence par son identifiant',
    })
    @ApiResponse({status: 200, type: LicenceEntity, isArray: false, description: 'Renvoie une licence'})
    public async get(@Param('id') id: string): Promise {
        return await this.repository.createQueryBuilder().where('id = :id', {id}).getOne();
    }

    @ApiOperation({
        operationId: 'getAllLicences',
        title: 'Rechercher toutes les licences ',
        description: 'Renvoie toutes les licences',
    })
    @ApiResponse({status: 200, type: LicenceEntity, isArray: true, description: 'Liste des licences'})
    @Get()
    public async getAllLicences(): Promise<LicenceEntity[]> {
        return this.repository.find();
    }

    @ApiOperation({
        operationId: 'getPageSizeLicencesForPage',
        title: 'Rechercher par page les licences ',
        description: 'Recherche paginée utilisant currentPage, pageSize, orderDirection, orderBy et Filters',
    })
    
    @Post()
    @ApiOperation({
        operationId: 'create',
        title: 'Cree une nouvelle licence',
    })
    public async create(@Body() licence: LicenceEntity): Promise {
        const newLicence = new LicenceEntity();
        newLicence.licenceNumber = licence.licenceNumber;
        newLicence.name = licence.name;
        newLicence.firstName = licence.firstName;
        newLicence.gender = licence.gender.toUpperCase();
        newLicence.club = licence.club;
        newLicence.dept = licence.dept;
        newLicence.birthYear = licence.birthYear;
        newLicence.catea = licence.catea ?
            licence.gender.toUpperCase() === 'F' ? 'F' + licence.catea.toUpperCase() : licence.catea.toUpperCase()
            : '';
        newLicence.catev = licence.catev.toUpperCase();
        newLicence.fede = FederationEntity[licence.fede.toUpperCase()];
        await this.entityManager.save(newLicence);
    }
}

Le code parle de lui-même, les dépendances sont injectées au constructeur avec les décorateurs @InjectRepository et @InjectEntityManager. Notez que dans la pratique ce sont généralement les services qui ont la charge de réaliser les accès en base pour permettre de mutualiser cette couche entre plusieurs contrôleurs. Dans notre cas, l’application étant constituée pour l’essentiel de méthodes CRUD nous avons placé les requêtes directement dans les contrôleurs.

@InjectRepository et @InjectEntityManager sont deux décorateurs du connecteur NestJS-Typeorm. Tous les autres décorateurs du code source précédent sont liés à la documentation Swagger et aux contrats de services. Les repositories jouent le même rôle que les repositories dans Spring Data, c’est l’implémentation du pattern Repository bien connu du monde des architectures d’entreprise.

Les contrôleurs sont essentiels dans la philosophie NestJS (ou TS.ED). On utilise des contrôleurs NestJS pour découpler le code du Framework sous-jacent à l’aide des fameux décorateurs, c’est un plus indéniable par rapport à l’utilisation directe d’un outil (on parlerait de « serveur d’application » dans le monde Java). Dans le cas d’Open Dossard, nous avons opté pour ExpressJS. Si vous souhaitez gagner en performances avec Fastify, il suffit simplement de paramétrer Fastify dans NestJS.

« Nest aims to be a platform-agnostic framework. Platform independence makes it possible to create reusable logical parts that developers can take advantage of across several different types of applications. Technically, Nest is able to work with any Node HTTP framework once an adapter is created. There are two HTTP platforms supported out-of-the-box: express and fastify. You can choose the one that best suits your needs »

Le mapping objet/relationnel avec TypeORM

Pour faire synthétique TypeORM est l’Hibernate du monde JS. Toute la philosophie de TypeORM est héritée des outils du même type existant dans le monde Java et .NET. Là encore le mécanisme est le même avec l’utilisation de décorateurs pour annoter les entités avec @Entity.

import {Column, Entity, Index, PrimaryGeneratedColumn} from 'typeorm';
import {ApiModelProperty} from '@nestjs/swagger';

@Entity({name: 'club'})
export class ClubEntity {
    @ApiModelProperty()
    @PrimaryGeneratedColumn()
    public id: number;
    @ApiModelProperty()
    @Column({nullable: true})
    @Index()
    public shortName: string;
    @ApiModelProperty()
    @Column({nullable: true})
    public dept: string;
    @ApiModelProperty()
    @Column({nullable: false})
    public longName: string;
}

Les décorateurs @ApiModelProperty de Swagger indiquent simplement que l’objet en question fera partie d’un contrat de service en tant que modèle de données (https://swagger.io/docs/specification/data-models/) transféré sur le Front. C’est de cette manière qu’il est possible de typer le modèle du domaine côté client. Une fois le SDK généré, le client récupère l’objet suivant (le code source est généré à chaque changement des contrats OpenAPI et placé dans un répertoire spécifique) :

/**
 * Generated Code
 * @export
 * @interface ClubEntity
 */
export interface ClubEntity {
    /**
     * @type {number}
     * @memberof ClubEntity
     */
    id: number;
    /**
     * @type {string}
     * @memberof ClubEntity
     */
    shortName: string;
    /**
     * @type {string}
     * @memberof ClubEntity
     */
    dept: string;
    /**
     * @type {string}
     * @memberof ClubEntity
     */
    longName: string;
}

Une fois générée, cette interface n’est plus polluée par les décorateurs ni les éventuelles dépendances techniques que pourrait avoir l’entité sur le Backend. C’est l’intérêt principal de la génération de code (SDK).

Bien évidemment, l’usage d’objets de transfert (DTO) est possible (et conseillé), c’est le cas des classes telles que Filter, Search ou RaceRow.

La gestion de la sécurité

Authentification

L’authentification fait partie des briques essentielles d’une application, nous avons opté dans Open Dossard pour le connecteur Passport.js avec JWT. La première fois que l’utilisateur se connecte, il appelle un service d’authentification (via HTTPS) sous la forme d’une requête Post HTTP. Le service s’appuie ensuite sur le Framework Passport.js qui propose un nombre incalculable de stratégies d’authentifications  (facebook, google, local, openid, saml, jwt, github,…). Une fois l’utilisateur vérifié, un jeton JWT est généré et propagé dans chaque appel de service à l’aide du SDK client. Voici les 2 classes en charge de l’authentification côté serveur.

import {ExtractJwt, Strategy, VerifiedCallback} from 'passport-jwt';
import {PassportStrategy} from '@nestjs/passport';
import {Injectable} from '@nestjs/common';
import {jwtConstants} from '../util/constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor() {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: jwtConstants.secret,
        });
    }

    async validate(payload: any): Promise<any> {
        return payload ;
    }
}

Et la classe chargée de valider les credentials :

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import {Injectable, Logger, UnauthorizedException} from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
    constructor(private readonly authService: AuthService) {
        super({
            usernameField: 'email',
            passwordField: 'password',
        });
    }

    async validate(username: string, password: string): Promise<any> {
        const user = await this.authService.validateUser(username, password);
        if (!user) {
            throw new UnauthorizedException();
        }
        return user;
    }
}

Ce connecteur PassportJS est un peu le pendant de Spring Security dans le monde Java même si dans la pratique il y aurait énormément à dire sur l’implémentation réalisée (nécessité de passer par un mécanisme de Guards propriétaires, etc…). Spring Security propose une architecture bien mieux découplée et NestJS y aurait à gagner en s’en inspirant un peu.

Certificats SSL et HTTPS

OpenDossard utilise des clés lets encrypt automatiquement renouvelées en utilisant la méthode acme.sh. Nous aurons l’occasion de revenir sur cet outil génialissime qui simplifie grandement l’obtention des certificats et le workflow des challenges imposés par lets encrypt. Tous les échanges, que ce soit celui du site web institutionnel ou celui de l’application sont chiffrés. Nous avons également fait en sorte d’obtenir une note A+ sur ssllabs.com grâce à une configuration SSL adaptée (voir fichier ssl.conf).

L’excellente note sur sslabs nous a motivé pour réaliser l’équivalent sur https://www.securityheaders.com qui valide les entêtes HTTP sécurisés.
Là encore, la configuration en place a permit de verdir le tableau sans trop d’efforts.

N’hésitez surtout pas à vous appuyer sur ces fichiers de configuration, dans notre cas le fait de passer par un reverse proxy simplifie la donne car seule une sécurisation nginx est nécessaire.

Injection SQL, XSS

En tant qu’organisme formateur sur les dix points de l’OWASP il aurait été mal venu de proposer une application « vitrine » aussi minime soit-elle truffée de failles de sécurité. Ainsi, pour se prémunir contre l’injection SQL nous avons fait en sorte d’utiliser systématiquement les requêtes paramétrées de TypeORM (d’où l’intérêt au passage d’utiliser un outil).

Côté client, les attaques de type XSS sont mitigées par l’utilisation de ReactJS qui échappe systématiquement le contenu des champs textuels comme le précise la documentation : « By default, React DOM escapes any values embedded in JSX before rendering them. Thus it ensures that you can never inject anything that’s not explicitly written in your application. Everything is converted to a string before being rendered. This helps prevent XSS (cross-site-scripting) attacks. ».

Les fonctionnalités restreintes de l’application ne nous expose pas à l’élévation de privilège ou des failles tels que XXE.

Par ailleurs la clé de chiffrement du jeton JWT a été stockée dans un fichier sécurisé non exposé par le serveur web (l’équivalent du fichier env.dev pour la production) et tous les mots de passe ont été chiffrés avec bcryptjs.

Mais nul n’est infaillible, si vous détectez une quelconque vulnérabilité, n’hésitez pas à nous en faire part, ouvrir un ticket en ligne ou mieux, à nous proposer une PR.

Les tests 

Les tests unitaires

NestJS propose toute une panoplie de classes permettant de faire la glue entre le Framework et les tests unitaires. Supposons que nous souhaitions bouchonner l’accès aux données et tester de manière unitaire un contrôleur, voici le code qu’il nous faudrait écrire. La méthode Test.createTestingModule(…) permet de compartimenter sous la forme d’un module les tests unitaires et d’injecter toutes les dépendances requises. Ce code s’appuie sur Jest comme moteur de test.

import {Test} from '@nestjs/testing';
import {LicenceController} from './licence.controller';
import {getRepositoryToken} from '@nestjs/typeorm';
import {LicenceEntity} from '../entity/licence.entity';
import {Apiv2Module} from '../apiv2.module';
import {getManager, Repository} from 'typeorm';
import {AppModule} from '../app.module';
import {FederationEntity} from '../entity/federation.entity';

/**
 * Un exemple de test unitaire des controlleurs
 */
describe('LicencesController', () => {
    let licencesCtrl: LicenceController;
    let licencesRepo: Repository<LicenceEntity>;

    beforeEach(async () => {
        const module = await Test.createTestingModule({
            imports: [
                AppModule,
                Apiv2Module],
        }).compile();

        licencesRepo = module.get<Repository<LicenceEntity>>(getRepositoryToken(LicenceEntity));
        licencesCtrl = new LicenceController(licencesRepo, getManager());
    });

    describe('findAll', () => {
        it('renvoie toutes les licences', async () => {
            const result: LicenceEntity[] = [{
                id: 1,
                licenceNumber: '11111',
                name: 'nom',
                firstName: 'prenom',
                dept: '81',
                fede: FederationEntity.FSGT,
                gender: 'M',
                birthYear: '1900',
                catea: 'S',
                club: 'club',
                catev: '3',
            }];
            jest.spyOn(licencesRepo, 'find').mockImplementation(() => Promise.resolve(result));
            expect(await licencesCtrl.getAllLicences()).toBe(result);
        });
    });
});

Les tests d’intégration 

Les tests d’intégration complètent les tests unitaires en réalisant des tests end-to-end, c’est à dire prenant en compte toutes les couches de l’application sans bouchon.

Voici le code d’un test qui vérifie qu’une licence insérée est bien ajoutée en base de données.

import {Test} from '@nestjs/testing';
import {LicenceController} from '../src/controllers/licence.controller';
import {getRepositoryToken} from '@nestjs/typeorm';
import {LicenceEntity} from '../src/entity/licence.entity';
import {Apiv2Module} from '../src/apiv2.module';
import {EntityManager, getManager, Repository} from 'typeorm';
import {AppModule} from '../src/app.module';
import {ClubController} from '../src/controllers/club.controller';
import {ClubEntity} from '../src/entity/club.entity';

describe('E2E_Licences', () => {
    let licencesCtrl: LicenceController;
    let clubCtrl: ClubController;
    let clubRepo: Repository<ClubEntity>;
    let licencesRepo: Repository<LicenceEntity>;
    let entityManager: EntityManager;

    beforeEach(async () => {
        const module = await Test.createTestingModule({
            imports: [
                AppModule,
                Apiv2Module],
        }).compile();

        // Entity manager init, its just a sample, caution : the entity manager could not be shared between multiple tests
        entityManager = getManager();

        licencesRepo = module.get<Repository<LicenceEntity>>(getRepositoryToken(LicenceEntity));
        licencesCtrl = new LicenceController(licencesRepo, entityManager);
    });
    
    const createLicence = (firstName, name, catea, catev, gender, club, dept, fede): LicenceEntity => {
        const licence = new LicenceEntity();
        licence.firstName = firstName;
        licence.name = name;
        licence.gender = gender;
        licence.catea = catea;
        licence.catev = catev;
        licence.fede = fede;
        licence.club = club;
        licence.dept = dept;
        return licence;
    };

    /**
     * E2E Tests, we test here the end point controllers by hitting the DB
     */
    describe('createLicence', () => {
        it('create a licence and check it is inserted', async () => {
            const inseredLicence: LicenceEntity = createLicence('roger', 'dupont', 'V', '4', 'H', 'club', '31', 'FSGT');
            await licencesCtrl.create(inseredLicence);
            const allLicences = await licencesCtrl.getAllLicences();
            // @ts-ignore
            expect(allLicences[allLicences.length - 1].name).toBe('dupont');
        });
    });

});

Conclusion

Nous espérons réellement que cette application “Vitrine” contribuera à faire connaître l’étendue des richesses de NestJS et cette nouvelle façon de coder des architectures multi-couches. Tout y est, de l’environnement de développement homogène, performant et typé à la gestion des bases de données en passant par la sécurité. L’écosystème NodeJS est si riche qu’il est très facile de s’y perdre. Les Framework “Progressifs” réduisent le spectre des outils à maîtriser et encouragent au développement structuré d’architectures multi-tiers. Nul doute que ces outils vont énormément apporter au monde JS dans les années à venir et surtout encourager les nombreux développeurs Java ou .NET à s’y intéresser.

Tout le code énoncé dans cet article est disponible sur github sans restriction. Si nos compétences vous intéressent ou que vous souhaitez faire connaître NestJS au sein de votre société ou communauté, n’hésitez pas à nous contacter.

Sami Jaber
DNG Consulting
sami.jaber@dng-consulting.com