Portfolio de Dasek Joiakim - Semestre 4
Semestre 3
B2

Compétence B2

Être capable de choisir une architecture adaptée et de structurer son développement autour de celle-ci

Une architecture adaptée est une architecture qui répond aux besoins du projet.

Projet Koloka

Architecture

J'ai dû choisir pour le projet Koloka une architecture qui répondait aux besoins du projet. Lors de la phase de conception j'ai pris en compte le fait que le Product Owner voulait une application web en premier lieu puis mobile. J'ai donc choisi une architecture orientée service avec une API REST pour le backend et une application web pour le frontend.

Le backend est en effet un Headless CMS nommé Strapi (opens in a new tab) qui permet de gérer les données de l'application. Le frontend est une application web en Next.js (opens in a new tab) qui permet de consommer l'API REST de Strapi. Ce choix a été fait pour permettre une évolution future de l'application vers une application mobile. En effet, il "suffit" de développer une application mobile qui consomme la même API REST que l'application web.

J'ai effectué plusieurs recherche pour trouver la meilleure solution pour le backend. Le choix était conditionné par le temps de développement, la possibilité de customiser le backend et la possibilité de l'héberger sur un serveur en Self-hosted. La communauté était un point cructial, le fait que le projet Strapi (opens in a new tab) soit open-source et que le développeurs qui maintiennent le projet soient très actifs sur les demandes de la communauté.

Un autre choix aurait été Sanity (opens in a new tab) qui est un aussi un Headless CMS mais qui est hébergé sur le cloud. Ce choix aurait été plus rapide à mettre en place mais aurait été plus coûteux sur le long terme. De plus, la communauté est moins active que celle de Strapi. J'ai dû lire la documentation en profondeur pour chacun de ces deux outils pour pouvoir faire un choix éclairé. J'ai aussi fait des tests pour voir de manière pratique lequel avait une meilleure expérience développeur. Le choix final s'est porté sur Strapi car il répondait à tous les critères.

Voici un aperçu de Strapi :

Strategy

Ainsi qu'un aperçu de Sanity :

Sanity

Accessibilité

Dans le projet Koloka j'ai dû mettre en place un tunnel pour permettre à l'application web d'être disponible durant les weekly meeting. J'ai donc choisi Ngrok (opens in a new tab) qui permet de créer un tunnel vers un serveur en local. J'ai choisi cette solution car elle est simple à mettre en place et gratuite. J'ai dû lire la documentation pour pouvoir mettre en place le tunnel et le configurer pour qu'il soit disponible durant les weekly meeting.

Ngrok

Cette décision a été prise parce que je voulais éviter que le Product Owner ait à installer l'application en local pour pouvoir tester les fonctionnalités. Ngrok est donc un système qui va créer un tunnel chiffré entre mes containers Next.js ainsi que Strapi et un serveur distant qui a une nom de domaine public. Cela permet de rendre l'application accessible depuis n'importe où dans le monde, durant les weekly meeting.

Déployement

Pour le déploiement de l'application Web Koloka j'ai choisi d'utiliser AWS EC2. C'est un service d'Amazon qui permet de déployer des instances de serveurs. J'ai choisi ce service car il est gratuit pour un an et il me permet, pour mon apprentissage, de me familiariser avec les services d'Amazon. C'est un IaaS (Infrastructure as a Service). En réalité il faudrait trois instances pour déployer l'application web. Un pour l'environnement de développement, un pour l'environnement de staging et un pour l'environnement de production.

AWS Services

Pour l'instant un seul serveur est utilisé pour l'environnement de développement. Le semestre prochain je vais mettre en place l'utilisation d'un service d'AWS nommé Fargate qui permet de déployer des containers Docker. Cela permettra d'avoir un environnement qui se comportera comme dans notre environnement local.

De plus je parle dans ma lecture individuelle de Strapi et le déploiement sur AWS pour un environnement de production.

Docker

Pour le projet Koloka j'ai dû mettre en place un environnement de développement avec Docker. J'ai choisi Docker car il permet de mettre en place un environnement de développement qui est le même pour tous les développeurs. Cela permet d'éviter les problèmes de compatibilité entre les environnements de développement. J'ai dû lire la documentation de Docker pour pouvoir mettre en place l'environnement de développement. Le but d'implémenter docker dans le projet est pour une facilité de portabilité sur l'environnement de production mais aussi pour que le projet se comporte de la même manière sur tous les environnements. intégrer docker est aussi prévu pour le semestre prochain afin de faciliter le déploiement sur AWS avec une pipeline CI/CD.

J'ai utilisé docker-compose pour pouvoir orchestrer les différents containers. Un pour Strapi, un pour Next.js et un pour Ngrok ainsi que Grafana (opens in a new tab).

Docker

J'ai aussi effectué une sur lecture individuelle sur Docker Compose, plus précisément docker-compose et lors de la présentation du power-point, j'ai décidé de faire une démonstration pratique, un playground de 2h30 avec les camarades sur comment démarrer un projet, puis implémenter une composition de container pour qu'il puissent être portable et utilisable par n'importe quel développeur.

Docker Compose

Proket AKTS

Architecture

Pour le projet AKTS j'ai dû chercher différents patterns d'architecture pour pouvoir mettre en place le projet. J'ai fait de la recherche sur les différents pattern en lien avec le SDK Flutter (opens in a new tab). J'ai trouvé plusieurs pattern intéressant :

Nous devions chacun implémenter un pattern dans le projet AKTS et présenter aux autres membres du projet. J'ai donc choisi d'implémententer le pattern Repository (opens in a new tab) car il me semblait le plus adapté au projet. En effet, ce pattern est adapté pour les cas d'utilisations suivants :

  • Communiquer avec les API REST
  • Interagir avec des bases de données locales ou distantes (en l'occurence locale Realm (opens in a new tab))
  • Utiliser des API spécifiques à un appareil (par exemple, les autorisations, la caméra, la localisation, etc.)

Session de formation

Architecture logicielle

La session de formation sur l'architecture logicielle m'a permis de comprendre les différents patterns d'architecture logicielle. J'ai pu comprendre les avantages et les inconvénients de chaque pattern. J'ai pu comprendre que chaque pattern est adapté à un cas d'utilisation précis.

Introduction aux architectures orientées service

SOA est un cadre qui permet aux entreprises de concevoir des systèmes informatiques qui répondent aux besoins métier grâce à la mise à disposition de services logiciels réutilisables et bien définis. Cela favorise une meilleure alignement entre les processus d'affaires et la technologie. L'approche SOA intègre l'analyse métier, technique, informationnelle et organisationnelle pour améliorer la collaboration, la gouvernance, la performance et la qualité des services IT.

Lectures individuelles

SOA et Microservices

Lors de la présentation en deux parties sur les microservices ainsi que le SOA j'ai pu saisir l’importance de l'architecture orientée services (SOA) et des microservices dans le paysage de l'architecture d'entreprise moderne.

  • SOA et Microservices encouragent la modularité : en créant de petits services indépendants, les systèmes deviennent plus flexibles et faciles à maintenir.
  • Meilleure scalabilité : Les entreprises peuvent adapter leur infrastructure facilement pour gérer des charges variables.
  • Réutilisation et intégration des services : Cela évite la redondance du code et simplifie l'intégration avec d' autres applications et services.
  • Agilité métier : SOA et microservices permettent une réponse rapide aux changements des besoins métier grâce à une architecture flexible.
  • Déploiement continu et DevOps : Les conteneurs et les microservices sont au cœur des pratiques DevOps, favorisant le déploiement continu et l'intégration continue.
  • Clarté fonctionnelle et opérationnelle : Chaque service a une fonction claire liée aux besoins métier, facilite la compréhension et la prise en charge opérationnelle.
  • Résilience et maintenabilité : La panne ou la mise à jour d'un service a moins d'impact sur l'ensemble du système, renforçant la robustesse de l’architecture.

IaaS, PaaS et SaaS

De plus la lecture individuelle sur IaaS, PaaS et SaaS m'a permis de comprendre la granularité des services proposés par les fournisseurs de cloud. J'ai pu comprendre que chaque service est adapté à un cas d'utilisation précis. J'ai pu comprendre que les services IaaS sont les plus flexibles et les plus complexes à mettre en place. Les services PaaS sont plus simples à mettre en place mais sont moins flexibles. Les services SaaS sont les plus simples à mettre en place mais sont les moins flexibles.

GraphQL vs RESTful

J'ai présenté et utiliser GraphQL dans le projet Koloka. J'ai donc dû faire une lecture individuelle sur GraphQL pour pouvoir comprendre le fonctionnement de GraphQL et ses avantages par rapport à RESTful.

Projet Koloka

Architecture cloud

Pour ce qui est de la pratique, j'ai mis en place une architecture cloud pour le projet Koloka. J'ai choisi et implémenté une architecture cloud pour le projet. Ce choix a été fait pour plusieurs raisons.

J'ai aussi dû implémenter la consommation d'un service de streaming de données en temps réel pour la fonctionnalité de messagerie instantanée. L'utilisation et la mise en place de Pusher (opens in a new tab) m'a permis de comprendre le fonctionnement d'un service de streaming de données en temps réel.

Session de formation

Architecture logicielle

Dans la session de formation sur l'architecture logicielle j'ai pu comprendre les différents patterns d'architecture :

  • Architecture Monolithique:

    • Toutes les fonctionnalités sont intégrées dans une seule et unique application.
    • Choix adapté pour des applications simples et lorsque la rapidité de déploiement est une priorité.
  • Architecture en Couches (Layered architecture):

    • Division de l'application en couches avec des responsabilités distinctes (présentation, logique métier, accès aux données, etc.).
    • Bon choix pour des applications d'entreprise standard qui nécessitent une structure claire et qui sont maintenues par de grandes équipes.
  • Architecture orientée services (Service-Oriented Architecture, SOA):

    • Composée de services qui communiquent entre eux, souvent à travers un bus de services ou des appels directs.
    • À utiliser quand vous avez besoin d'intégrer des applications hétérogènes ou prévoir une réutilisation de business logics.
  • Architecture basée sur les événements (Event-Driven Architecture):

    • Les composants réagissent aux événements et sont souvent déclenchés par des actions externes ou internes.
    • Pertinent pour les systèmes asynchrones à forte scalabilité ou pour les applications réactives.
  • Architecture Microservices:

    • Application divisée en petites parties autonomes, chacune ayant une responsabilité unique et pouvant être déployée indépendamment.
    • Idéale pour les systèmes complexes et évolutifs, où les équipes travaillent en parallèle sur différentes fonctionnalités.
  • Architecture Web Services (SOAP, REST, GraphQL):

    • Services exposés via le protocole HTTP, permettant l'interopérabilité entre les systèmes informatiques sur Internet.
    • Choisir en fonction de la nécessité d'intégrations web accessibles, standardisées et pouvant fonctionner avec différentes technologies client.

Connaitre et maîtriser les principaux design patterns

Un design pattern est une solution à un problème récurrent. Il permet de résoudre un problème de manière efficace et élégante. Il permet aussi de faciliter la compréhension du code par les autres développeurs.

Lecture individuelle

Design pattern

Lors de la lecture individuelle sur les design patterns j'ai pu comprendre et implémenter plusieurs design patterns, classés en trois catégories :

  • Création : Ces design patterns fournissent un mécanisme de création d'objets qui augmente la flexibilité et la réutilisation du code.
  • Structure : Ces design patterns expliquent comment créer des relations entre les objets et comment les organiser pour simplifier la structure du code.
  • Comportement : Ces design patterns sont particulièrement utiles pour communiquer entre les objets et les classes d'une manière simple et flexible.

Koloka Design pattern - Component

Dans le projet Koloka j'ai dû mettre en place un design pattern pour le frontend. J'ai choisi d'utiliser le design pattern Component (opens in a new tab) qui est un design pattern de React. Ce design pattern permet de découper l'application en composants. J'ai aussi utilisé le design pattern Atomic Design (opens in a new tab) qui permet de découper les composants en plusieurs niveaux.

Voici un composant parent :

<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:max-w-7xl lg:px-8">
    <Title/>
    <Search/>
    <MobileFilter mobileFiltersOpen={mobileFiltersOpen} setMobileFiltersOpen={setMobileFiltersOpen}
                  filters={filters}/>
    <Filter filters={filters} setMobileFiltersOpen={setMobileFiltersOpen} sortOptions={sortOptions}/>
    <PropertyGrid/>
</div>

Voici un composant enfant :

const Title = () => {
    return (
        <div className="py-24 text-center">
            <h1 className="text-4xl font-bold tracking-tight text-gray-900">Properties</h1>
            <p className="mx-auto mt-4 max-w-3xl text-base text-gray-500">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit bortis arcu enim urna adipiscing praesent.
            </p>
        </div>
    )
};
 
export default Title;

Ces composants sont ensuite utilisés dans d'autres composants. Par exemple le composant Title peut-être utilisé dans d'autres parties de l'application. Ce design pattern permet de découper l'application en composants réutilisables et réduire la redondance du code. C'est aussi efficace pour la maintenance du code !

Koloka Design pattern - Atomic Design - Jotaï

Jotaï est state management pour React, il permet de gérer l'état de l'application de manière efficace et élégante.

Voici un exemple de son utilisation, la définition de son store global :

import {atom} from "jotai";
 
const chatsAtom = atom({myApplications: [], myOffers: []});
 
export {chatsAtom};

Voici un exemple de son utilisation, la définition d'un composant qui utilise le store global :

const setChats = useSetAtom(chatsAtom);
const setCurrentChat = useSetAtom(currentChatAtom);
const currentChannel = useAtomValue(currentChannelAtom);
usePreloadChat(currentChannel, chats, setCurrentChat);
const handleChatSelection = (chat) => {
    setCurrentChat(chat);
    setChats(prevChats => {
        return Object.keys(prevChats).reduce((acc, key) => {
            acc[key] = prevChats[key].map(chatItem => {
                if (chatItem.idChannel === chat.idChannel) {
                    return {
                        ...chatItem,
                        hasNotification: false
                    };
                } else {
                    return chatItem;
                }
            });
            return acc;
        }, {});
    });
};

Koloka Design pattern - Singleton

J'ai aussi utilisé le Singleton dans le projet Koloka. J'ai utilisé le Singleton pour la créer une seule instance Websocket pour la fonctionnalité de messagerie instantanée.

export const usePusher = () => {
    const pusher = useMemo(() => new Pusher(process.env.NEXT_PUBLIC_APP_KEY, {
        cluster: process.env.NEXT_PUBLIC_CLUSTER,
        forceTLS: true,
    }), []);
};

L'utilisation du Hook useMemo permet de créer une seule instance de Pusher. Cela permet d'éviter de créer plusieurs instances de Pusher et donc de créer plusieurs Websocket. Donc d'éviter de créer plusieurs connexions au serveur de Pusher entre les re-renders.

AKTS Design pattern - Adapter

Dans le projet AKTS j'ai dû mettre en place un design pattern pour la connexion BLE (Bluetooth Low Energy). Sachant qu'il pourrait avoir différents périphériques auxquels se connecter, il est fort probable qu'ils aient différentes manière de communiquer ! J'ai implémenter le design pattern Adapter pour que quelque soit le périphérique, le nom des méthodes pour initier un scan, une connexion ou une récupération de données doit être la même, mais l'implémentation de celles-ci va certainement être différente !

J'ai du créer une interface abstraite qui permet de définir les méthodes que doit implémenter une connexion pour un périphérique spécifique. Je dois donc utiliser non pas uniquemen une interface mais une classe abstraite qui permet de définir certaines méthodes concrête qui vont existé pour tous les périphériques et une interface qui hérite de cette classe abstraite et qui permet de définir les méthodes qui doivent être implémentées pour chaque périphérique !

abstract class InterfaceBLE {
  final _loggerStreamController = StreamController<List<Logger>>.broadcast();
 
  Stream<List<Logger>> get loggerStream => _loggerStreamController.stream;
  final _loggerDataStreamController =
  StreamController<Map<String, dynamic>>.broadcast();
 
  Stream<Map<String, dynamic>> get loggerDataStream =>
      _loggerDataStreamController.stream;
 
  Future<void> scanLogger(List<String> loggerMacAdressVisible);
 
  void stopScan() {
    FlutterBluePlus.stopScan();
  }
 
  void dispose() {
    _loggerStreamController.close();
  }
 
  void connectLogger(Logger logger, Function setState, BuildContext context,
      RuuviTagModel1 ruuviTagModel1);
 
  Future<void> retrieveDataLogger(ScanResult r,
      int timestampStart,
      int timestampEnd,
      Function(String) updateProgressStep,
      Function(int) updateProgressValue,);
 
  List<int> getTimestamps(int timestampStart, int timestampEnd) {
    print("Timestamp start: $timestampStart");
    print("Timestamp end: $timestampEnd");
    Uint8List startBytes = Uint8List(4)
      ..buffer.asByteData().setUint32(0, timestampEnd, Endian.big);
    Uint8List endBytes = Uint8List(4)
      ..buffer.asByteData().setUint32(0, timestampStart, Endian.big);
 
    return [...startBytes, ...endBytes];
  }
}

Voici un exemple d'implémentation de cette interface abstraite pour un modèle de périphérique spécifique :

class RuuviTagModel1 implements InterfaceBLE {
  final _loggerStreamController = StreamController<List<Logger>>.broadcast();
 
  Stream<List<Logger>> get loggerStream => _loggerStreamController.stream;
 
  final _loggerDataStreamController =
  StreamController<Map<String, dynamic>>.broadcast();
 
  Stream<Map<String, dynamic>> get loggerDataStream =>
      _loggerDataStreamController.stream;
 
  double? _currentTemperature;
  double? _currentHumidity;
  double? _currentPressure;
 
  Future<void> scanLogger(List<String> loggerMacAdressVisible) async {
    List<Logger> loggerList = [];
    Set<DeviceIdentifier> loggerAlreadyScanned = {};
 
    FlutterBluePlus.scanResults.listen((results) async {
      for (ScanResult r in results) {
        // etc...
      }
    });
  }
// etc...
}

Maîtriser les structures de données avancées

Le structures de données avancées sont représentées de plusieurs manières. En effet, il existe plusieurs types de structures de données avancées comme les arbres, les graphes, les matrices etc...

Projets

Koloka - Système de messagerie

Dans le projet Koloka j'ai dû mettre en place un système de messagerie instantanée. Pour cela j'ai dû organiser en amont la structure des chats contenant des messages avec des méta-données comme le nom de l'expéditeur, le nom du destinataire, le contenu du message, la date d'envoie du message ou encore si le message a été lu ou non.

Voici un exemple pour la fonctionnalité du chat de l'application :

usePusher(chats, 'message', ({channel, data}) => {
    const newPayload = {content: data.payload, me: false};
    let updatedChat = {};
    setChats(prevChats => {
        return Object.keys(prevChats).reduce((acc, key) => {
            acc[key] = prevChats[key].map(chat => {
                if (chat.idChannel === channel) {
                    updatedChat = {
                        ...chat,
                        messages: chat.hasOwnProperty("messages") ? [...chat.messages, newPayload] : [newPayload],
                        hasNotification: true
                    };
                    setCurrentChat(prevChat => {
                        if (prevChat && prevChat.idChannel === channel) {
                            updatedChat.hasNotification = false;
                            return {
                                ...prevChat,
                                messages: [...prevChat.messages, newPayload]
                            };
                        } else return prevChat;
                    });
                    return updatedChat;
                } else {
                    return chat;
                }
            });
            return acc;
        }, {});
    });
}, requireSyncChats)

AKTS - BLE

Dans le projet AKTS j'ai dû mettre en place un système de récupération de données depuis un périphérique BLE. Pour cela j'ai dû explorer la structure de données reçue depuis le périphérique. J'ai dû comprendre comment les données étaient structurées pour pouvoir les parser et les afficher dans l'application.

Map<String, dynamic> processData(List<int> data) {
  Map<String, dynamic> result = {};
  result['progressValueTimeStamp'] = 0;
 
  if (data.isNotEmpty && data[0] == 58 && data.length == 11) {
    ByteData byteData = ByteData.sublistView(Uint8List.fromList(data));
 
    int dat0 = byteData.getUint8(0);
    int dat1 = byteData.getUint8(1);
    int dat2 = byteData.getUint8(2);
    int dat3 = byteData.getUint32(3, Endian.big);
    int dat4 = byteData.getUint32(7, Endian.big);
 
    result['allcomplete'] = false;
    if (dat3 == RuuviConstant.DATALOG_END &&
        dat4 == RuuviConstant.DATALOG_END) {
      print("Log: end of receiving data");
      result['allcomplete'] = true;
    } else {
      result['complete'] = false;
      if (dat1 == RuuviConstant.HUMIDITY) {
        _currentHumidity = dat4 / 100.0;
        _currentPressure = null;
        _currentTemperature = null;
      } else if (dat1 == RuuviConstant.AIR_PRESSURE) {
        _currentPressure = dat4 / 100.0;
        _currentTemperature = null;
      } else if (dat1 == RuuviConstant.TEMPERATURE) {
        _currentTemperature = dat4 / 100.0;
      } else if (dat1 == RuuviConstant.ALL_SENSORS) {}
 
      DateTime date = DateTime.fromMillisecondsSinceEpoch(dat3 * 1000);
      result['progressValueTimeStamp'] = dat3;
 
      if (_currentTemperature != null &&
          _currentHumidity != null &&
          _currentPressure != null) {
        DataLogger loggerData = DataLogger(
          timestamp: dat3,
          humidity: _currentHumidity!,
          temperature: _currentTemperature!,
          pressure: _currentPressure!,
        );
 
        print(DateTime.fromMillisecondsSinceEpoch(loggerData.timestamp * 1000)
            .toString() +
            " - TEMP: " +
            loggerData.temperature.toString() +
            "°C"
                " - HUM: " +
            loggerData.humidity.toString() +
            "%"
                " - PRESS: " +
            loggerData.pressure.toString() +
            "hPa");
 
        result['complete'] = true;
        result['data'] = loggerData;
      }
    }
  }
 
  return result;
}

Maîtriser les concepts des librairies et composants (principes SOLID, création de logiciels selon architecture modulaire, partage de briques logicielles)

La définition des principes SOLID est la suivante :

  • Single Responsibility Principle : Une classe ne doit avoir qu'une seule responsabilité.
  • Open/Closed Principle : Une classe doit être ouverte à l'extension mais fermée à la modification.
  • Liskov Substitution Principle : Les objets d'un programme doivent être remplaçables par des instances de leurs
  • Interface Segregation Principle : Les interfaces doivent être spécifiques aux besoins des clients.
  • Dependency Inversion Principle : Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau.

Projet Koloka

Dans le projet Koloka, le principe SOLID s'applique dans le Component pattern de React. En effet, chaque composant est indépendant et peut être réutilisé dans d'autres composants. Cela permet de réduire la redondance du code et de faciliter la maintenance du code.

Il y a aussi Jotaï qui est basé sur le pattern d'atomicité. Il permet de découper le code en plusieurs niveaux.

De plus le projet Koloka contient des dépendances dont j'ai dû choisir minutieusement, leurs intégrations dans le projet et leurs utilisations a dû se faire de manière réfléchie et en collaboration avec les autres membres du projet.

Koloka est un projet avec une architecture modulaire dont le backend et frontent est clairement distinct. Le but du semestre prochain sera de créer un mono-repo pour que le dossier de dépendance se partage pour ces deux frameworks afin d'éviter d'alourdir le projet, de faciliter la maintenance, de faciliter le déploiement mais aussi d'éviter les doublons de dépendances, d'où le partage de briques logicielles entre le backend et le frontend.

J'utilise aussi un gestionnaire de dépendances pour le projet Koloka. J'ai choisi NPM (opens in a new tab) car il est le plus utilisé dans le monde du développement web sur le runtime Node.js. Mais j'ai toujours pris l'initiative d'utiliser d'autres gestionnaires de dépendances pour pouvoir comparer les avantages et les inconvénients de chacun. Chacun a sa stratégie de gestion des dépendances, son système de versioning, son système de publication, etc...

Projet AKTS

Dans le projet AKTS, la structure du projet est décomposée en plusieurs modules. Chaque module a une responsabilité précise. Par exemple le module BLE a pour responsabilité de gérer la connexion BLE. Le module screen a pour responsabilité de gérer les écrans de l'application. Le module repository a pour responsabilité de gérer les requêtes HTTP vers l'API REST / requête vers la base de données locale Realm (opens in a new tab) mais aussi la logique métier. On pourrait aussi implémenter un dossier provider qui aurait pour responsabilité de faire écouler les données entre les composants.

J'utilise le gestionnaire de dépendances Pub (opens in a new tab) qui est le gestionnaire de dépendances officiel de Flutter.

Lecture individuelle

SOLID

Lors de la lecture individuelle sur SOLID j'ai pu comprendre les principes SOLID et leurs applications dans le monde du développement logiciel. J'ai pu comprendre que ces principes comme décrits ci-dessus.

Mettre en œuvre la sécurité dans la construction de services (sécurité applicative)

La sécurité applicative est la sécurité au niveau de l'application. Cela concerne la sécurité des données, la sécurité des utilisateurs, la sécurité des transactions, etc...

Projet Koloka

J'ai dû implémenter une couche sécurité sur la couche frontend, API / controller et base de donnée par exmeple pour la fonctionnalité de messagerie instantanée.

Auth.js

Auth.js est une librairie d'authentification pour le WEB, je l'ai configuré pour qu'elle puisse fonctionner avec 0Auth. C'est l'authentification par un service tiers. Dans le cas de Koloka, c'est l'authentification par Google et Facebook.

J'ai donc intégré la librairie et implémentée la stratégie avec JSON Web Token pour éviter de submerger la base de donnée avec des requêtes HTTP pour vérifier l'authentification de l'utilisateur.

Voici un exemple de la stratégie d'authentification avec JSON Web Token :

JSON Web Token

Strapi Policies

Je réfère les policies qui sont les règles des requêtes entrantes avec qu'elles passent dans le controller de l'API.

Middleware forwarding

Le middleware c'est une couche de sécurité qui permet de vérifier des éléments comme les policies dans Strapi. J'ai implémenté un middleware pour certaines pages de l'application qui doivent être accessibles uniquement par les utilisateurs authentifiés.

Voici un exemple de middleware :

export default getServersidePropsWithAuth(async (context) => {
    const {req, res} = context;
    const session = await getServerSession(req, res, authOptions);
    if (!session) {
        return {
            redirect: {
                destination: '/login',
                permanent: false,
            },
        };
    }
    return {
        props: {},
    };
});

Sauvegarde de base de donnée

J'ai configurer un sauvegarde journalière sur AWS pour la base de donnée de Koloka. Cela permet de pouvoir restaurer la base de donnée en cas de problème.

Sauvegarde

Frontend

Dans le code suivant, j'ai dû mettre en place une sécurité au niveau du frontend pour éviter que l'utilisateur puisse envoyer un message vide. J'ai aussi dû mettre en place une sécurité pour éviter que l'utilisateur puisse envoyer un message alors qu'un message est en cours d'envoie.

const Message = () => {
    const [currentChat, setCurrentChat] = useAtom(currentChatAtom);
    const formRef = useRef(null);
    const [isSending, setIsSending] = useState(false);
 
    const handleSendMessage = async (event, form) => {
        event.preventDefault();
        const message = form.message.value;
        if (message === '') return;
        setIsSending(true);
        const currentMessage = await sendMessage(form);
        setIsSending(false)
 
        if (currentMessage) {
            const newMessage = {content: currentMessage.content, me: currentMessage.me};
            setCurrentChat({...currentChat, messages: [...currentChat.messages, newMessage]});
        }
    }
 
    const handleKeyPress = async (event) => {
        if (event.key === 'Enter' && !event.shiftKey && isSending === false) {
            await handleSendMessage(event, formRef.current);
        }
    }
 
    const handleSubmit = async (event) => {
        await handleSendMessage(event, formRef.current);
    }
 
    return (
        <form ref={formRef} className="grid w-full gap-2" onSubmit={handleSubmit}>
            <Textarea disabled={isSending} name='message' placeholder="Type your message here." required={true}
                      onKeyPress={handleKeyPress}/>
            <input type="hidden" name="channel" value={currentChat.idChannel}/>
            <Button type='submit' disabled={isSending}>Send message</Button>
        </form>
    )
}
 
export default Message;

API / Controller

Dans le code suivant, j'ai dû mettre en place un workflow de sécurité au niveau de l'API / controller, la fonction :

  1. Récupère une session utilisateur avec la fonction getServerSession.
  2. Si la session existe, elle continue, sinon elle renvoie une réponse avec le statut 401 (non autorisé).
  3. Elle récupère le payload (contenu du message) et le channel (canal de discussion) depuis le corps de la requête. S'ils sont absents, elle renvoie une réponse avec le statut 400 et un message d'erreur.
  4. Elle construit un nouvel objet newPayload avec le contenu du message et l'identifiant de l'utilisateur.
  5. Elle fait une requête HTTP POST pour récupérer l'identifiant du chat à partir du channel et de idUser.
  6. Si le chat n'est pas trouvé, elle renvoie une réponse avec le statut 404 et un message d'erreur.
  7. Elle récupère les données du chat chat et envoie un message en faisant une autre requête HTTP POST.
  8. Elle déclenche un événement message sur l'objet pusher avec le newPayload.
  9. Finalement, elle définit la propriété me des données renvoyées à true et renvoie une réponse avec le statut 200 et les données en format JSON.
export default async function handler(req, res) {
    const session = await getServerSession(req, res, authOptions);
    if (session) {
        const {payload, channel} = req.body;
        if (!payload || !channel) {
            res.status(400);
            res.json({error: "Missing payload or channel"});
            return;
        }
        const newPayload = {
            payload,
            from: {
                id: session.token.id,
            },
        };
        const retrieveChatId = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_API_URL}/api/chat/retrieveChatId`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${process.env.STRAPI_API_KEY}`,
            },
            body: JSON.stringify({
                data: {
                    channel,
                    idUser: session.token.id,
                }
            }),
        });
        if (retrieveChatId.status !== 200) {
            res.status(404);
            res.json({error: "Chat not found"});
        }
        const chat = await retrieveChatId.json();
        const saveMessage = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_API_URL}/api/messages`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${process.env.STRAPI_API_KEY}`,
            },
            body: JSON.stringify({
                data:
                    {
                        chat: chat.idChat,
                        idUser: session.token.id,
                        content: newPayload.payload,
                    }
            }),
        });
        await pusher.trigger(channel, 'message', newPayload);
        const data = await saveMessage.json();
        data.me = true;
        res.status(200).json(data);
    } else {
        res.status(401);
    }
}

Base de donnée

Dans le schéma de base de donnée suivant, j'ai dû mettre en place une sécurité au niveau de l'intégrité des données pour éviter que les données soient corrompues ou que des données soient manquantes etc... Les données doivent être non-null, au moins 1 caractère, etc... Sinon mysql renvoie une erreur.

Voici le schéma de la base de donnée pour la fonctionnalité de messagerie instantanée :

Database schema

Article réflexif

Mon dernier artcile réflexif portait sur la sécurité applicative. J'ai travailler sur la problématique suivante :

  • Assurance qualité (QA) et tests sont essentiels dans le développement d'applications web, face à l'augmentation de la complexité et des attentes des utilisateurs.
  • Importance de garantir le fonctionnement, la performance et la sécurité des applications web.
  • Objectif de l'article : explorer la place des tests et du QA dans le développement agile, à travers le cas pratique de "Koloka".
  • Méthode utilisée : démarche structurée basée sur la méthode expérientielle de Kolb.
  • Présentation de l'expérience concrète avec le développement de "Koloka" et prise de conscience de l'importance des tests après détection de bugs et de questions de sécurité.
  • Nécessité des tests et assurance qualité soulignée par une veille technologique.

Session de formation

J'ai eu une session de formation sur la sécurité applicative. J'ai pu pu comprendre les différents types de vulnérabilités et les différentes attaques possibles sur une application web. L'avantage de cette session de formation est que j'ai eu un playground avec plusieurs vulnérabilités et attaques possibles. Cela m'a permis de comprendre comment fonctionne les attaques et comment les éviter. Voici un bref apperçu de cette session de formation :

Environnement Laboratoire

  • Configuration de Docker, Git-Lab, et VS Code pour l'environnement de laboratoire.

1ère ½ Journée : Sécurité Web et Cryptographie

  • Étude des attaques web (clickjacking, XSS).
  • Principes du hachage : sécurité, gestion des collisions, effet avalanche.
  • Technique de salage pour renforcer la sécurité des mots de passe.
  • Gestion sécurisée des cookies de session et utilisation de HTTPS.

Cryptographie

  • Cryptage symétrique avec AES, gestion des clés partagées.
  • Stream Cipher et Diffie-Hellman Key Agreement.
  • Cryptographie asymétrique pour une sécurité accrue.

Autorité de Certification et Sécurité Web

  • Exemple de gestion de certificat SSL avec Swisscom.ch.
  • Importance du reverse proxy, utilisation de Flask.
  • Distinction entre différents niveaux de certifications SSL/TLS.
  • Gestion des mots de passe et prévention des vulnérabilités web (Cross-Site Request Forgery, Broken Access Control).

Lecture individuelle

Lors de la lecture individuelle sur les Web Vulnerabilities j'ai pu comprendre les différentes vulnérabilités axées sur les applications web. J'ai pu retenir ces différents points :

  • OWASP : Organisation à but non lucratif qui se consacre à la sécurité des applications web.
  • Authentication Bypass : Une vulnérabilité qui permet à un attaquant de contourner les mécanismes d'authentification pour accéder à des ressources non autorisées.
  • IDOR (Insecure Direct Object Reference) : Une vulnérabilité où un attaquant peut accéder ou modifier des objets non autorisés en manipulant directement les références aux objets.
  • File Inclusion : Une vulnérabilité qui permet à un attaquant d'inclure et d'exécuter du code malveillant à partir de fichiers distants ou locaux.
  • SSFR (Server-Side Request Forgery) : Une vulnérabilité où un attaquant peut forger des requêtes depuis le serveur pour accéder à des ressources internes ou externes.
  • Cross-site Scripting (XSS) : Une vulnérabilité qui permet à un attaquant d'injecter du code malveillant dans des pages web consultées par d'autres utilisateurs.
  • Command Injection : Une vulnérabilité qui permet à un attaquant d'exécuter des commandes système non autorisées sur le serveur.
  • SQL Injection : Une vulnérabilité qui permet à un attaquant d'injecter du code SQL malveillant pour manipuler la base de données.
  • Common API Security Problems : Problèmes de sécurité courants liés aux interfaces de programmation d'applications (API), tels que l'absence de validation d'entrées, l'authentification faible, l'autorisation inadéquate, etc.