Créer un serveur de notifications push

Dans cet atelier de programmation, vous allez créer un serveur de notifications push. Le serveur gérera la liste des abonnements push et leur enverra des notifications.

Le code client est déjà terminé. Dans cet atelier de programmation, vous allez travailler sur la fonctionnalité côté serveur.

Les notifications de l'application Glitch intégrée sont automatiquement bloquées. Vous ne pourrez donc pas prévisualiser l'application sur cette page. Voici la marche à suivre:

  1. Cliquez sur Remix to Edit (Remixer pour modifier) pour pouvoir modifier le projet.
  2. Pour prévisualiser le site, appuyez sur Afficher l'application. Appuyez ensuite sur Plein écran plein écran

L'application en ligne s'ouvre dans un nouvel onglet Chrome. Dans le Glitch intégré, cliquez sur View Source (Afficher la source) pour afficher à nouveau le code.

Tout au long de cet atelier de programmation, modifiez le code dans le Glitch intégré sur cette page. Actualisez le nouvel onglet contenant votre application en ligne pour voir les modifications.

Se familiariser avec l'application de démarrage et son code

Commencez par examiner l'UI cliente de l'application.

Dans le nouvel onglet Chrome:

  1. Appuyez sur Ctrl+Maj+J (ou Cmd+Option+J sur Mac) pour ouvrir les outils de développement. Cliquez sur l'onglet Console.

  2. Essayez de cliquer sur les boutons de l'interface utilisateur (consultez la console pour les développeurs Chrome pour obtenir le résultat).

    • L'option Enregistrer un service worker enregistre un service worker pour le champ d'application de l'URL de votre projet Glitch. L'option Annuler l'enregistrement d'un service worker supprime le service worker. Si un abonnement push lui est associé, il sera également désactivé.

    • S'abonner pour envoyer des notifications push crée un abonnement push. Elle n'est disponible que lorsqu'un service worker a été enregistré et qu'une constante VAPID_PUBLIC_KEY est présente dans le code client (nous reviendrons sur ce point ultérieurement). Vous ne pouvez donc pas cliquer dessus tout de suite.

    • Lorsque vous disposez d'un abonnement push actif, la fonctionnalité Notifier de l'abonnement en cours demande au serveur d'envoyer une notification à son point de terminaison.

    • Notifier tous les abonnements indique au serveur d'envoyer une notification à tous les points de terminaison d'abonnement de sa base de données.

      Notez que certains de ces points de terminaison peuvent être inactifs. Il est possible qu'un abonnement disparaisse au moment où le serveur lui envoie une notification.

Voyons ce qui se passe côté serveur. Pour afficher les messages à partir du code du serveur, consultez le journal Node.js dans l'interface Glitch.

  • Dans l'application Glitch, cliquez sur Outils -> journaux.

    Vous verrez probablement un message tel que Listening on port 3000.

    Si vous avez essayé de cliquer sur Envoyer une notification pour l'abonnement en cours ou Envoyer une notification à tous les abonnements dans l'interface utilisateur de l'application en ligne, le message suivant s'affiche également:

    TODO: Implement sendNotifications()
    Endpoints to send to:  []
    

Examinons maintenant un peu de code.

  • public/index.js contient le code client finalisé. Il détecte les fonctionnalités, enregistre et annule l'enregistrement du service worker, et contrôle l'abonnement des utilisateurs aux notifications push. Il envoie également au serveur des informations sur les abonnements nouveaux et supprimés.

    Étant donné que vous travaillerez uniquement sur les fonctionnalités du serveur, vous ne modifierez pas ce fichier (à part remplir la constante VAPID_PUBLIC_KEY).

  • public/service-worker.js est un service worker simple qui capture les événements push et affiche des notifications.

  • /views/index.html contient l'UI de l'application.

  • .env contient les variables d'environnement que Glitch charge sur votre serveur d'applications au démarrage. Vous allez renseigner .env avec les informations d'authentification pour l'envoi de notifications.

  • server.js est le fichier dans lequel vous allez effectuer la plupart de vos tâches au cours de cet atelier de programmation.

    Le code de démarrage crée un serveur Web Express simple. Quatre éléments TODO sont à votre disposition, marqués dans les commentaires du code avec TODO:. Vos tâches sont les suivantes :

    Dans cet atelier de programmation, vous allez examiner ces éléments TODO un par un.

Générer et charger les détails VAPID

Votre premier élément TODO consiste à générer les détails VAPID, à les ajouter aux variables d'environnement Node.js, et à mettre à jour le code client et le code du serveur avec les nouvelles valeurs.

Contexte

Lorsque les utilisateurs s'abonnent aux notifications, ils doivent faire confiance à l'identité de l'application et de son serveur. Les utilisateurs doivent également s'assurer que lorsqu'ils reçoivent une notification, celle-ci provient de l'application qui a configuré l'abonnement. Ils doivent également s'assurer que personne d'autre ne peut lire le contenu des notifications.

Le protocole qui assure la sécurité et la confidentialité des notifications push est appelé Voluntary Application Server Identification for Web Push (VAPID). VAPID utilise la cryptographie à clé publique pour valider l'identité des applications, des serveurs et des points de terminaison d'abonnement, ainsi que pour chiffrer le contenu des notifications.

Dans cette application, vous utiliserez le package npm web-push pour générer des clés VAPID, ainsi que pour chiffrer et envoyer des notifications.

Implémentation

Au cours de cette étape, vous allez générer une paire de clés VAPID pour votre application et les ajouter aux variables d'environnement. Chargez les variables d'environnement sur le serveur et ajoutez la clé publique en tant que constante dans le code client.

  1. Utilisez la fonction generateVAPIDKeys de la bibliothèque web-push pour créer une paire de clés VAPID.

    Dans server.js, supprimez les commentaires autour des lignes de code suivantes:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  2. Une fois que Glitch a redémarré votre application, les clés générées sont consignées dans le journal Node.js de l'interface Glitch (et non dans la console Chrome). Pour afficher les clés VAPID, sélectionnez Tools -> (Outils -> Journaux dans l'interface Glitch.

    Veillez à copier vos clés publique et privée à partir de la même paire de clés.

    Glitch redémarre votre application chaque fois que vous modifiez votre code. Par conséquent, la première paire de clés que vous générez peut disparaître de la vue comme suit.

  3. Dans .env, copiez et collez les clés VAPID. Placez les clés entre guillemets doubles ("...").

    Pour VAPID_SUBJECT, vous pouvez saisir "mailto:test@test.test".

    .env

    # process.env.SECRET
    VAPID_PUBLIC_KEY=
    VAPID_PRIVATE_KEY=
    VAPID_SUBJECT=
    VAPID_PUBLIC_KEY="BN3tWzHp3L3rBh03lGLlLlsq..."
    VAPID_PRIVATE_KEY="I_lM7JMIXRhOk6HN..."
    VAPID_SUBJECT="mailto:test@test.test"
    
  4. Dans server.js, mettez de nouveau en commentaire ces deux lignes de code, car vous n'avez besoin de générer les clés VAPID qu'une seule fois.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  5. Dans server.js, chargez les informations sur la propriété VAPID à partir des variables d'environnement.

    server.js

    const vapidDetails = {
      // TODO: Load VAPID details from environment variables.
      publicKey: process.env.VAPID_PUBLIC_KEY,
      privateKey: process.env.VAPID_PRIVATE_KEY,
      subject: process.env.VAPID_SUBJECT
    }
    
  6. Copiez et collez également la clé public dans le code client.

    Dans public/index.js, saisissez pour VAPID_PUBLIC_KEY la même valeur que celle que vous avez copiée dans le fichier .env:

    public/index.js

    // Copy from .env
    const VAPID_PUBLIC_KEY = '';
    const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...';
    ````
    

Implémenter une fonctionnalité pour envoyer des notifications

Contexte

Dans cette application, vous utiliserez le package npm web-push pour envoyer des notifications.

Ce package chiffre automatiquement les notifications lorsque webpush.sendNotification() est appelé. Vous n'avez donc pas à vous en soucier.

Le paramètre web-push accepte plusieurs options de notification. Vous pouvez, par exemple, joindre des en-têtes au message et spécifier l'encodage du contenu.

Dans cet atelier de programmation, vous n'utiliserez que deux options, définies avec les lignes de code suivantes:

let options = {
  TTL: 10000; // Time-to-live. Notifications expire after this.
  vapidDetails: vapidDetails; // VAPID keys from .env
};

L'option TTL (Time To Live) définit un délai d'expiration pour une notification. Cela permet au serveur d'éviter d'envoyer une notification à un utilisateur lorsqu'elle n'est plus pertinente.

L'option vapidDetails contient les clés VAPID que vous avez chargées à partir des variables d'environnement.

Implémentation

Dans server.js, modifiez la fonction sendNotifications comme suit:

server.js

function sendNotifications(database, endpoints) {
  // TODO: Implement functionality to send notifications.
  console.log('TODO: Implement sendNotifications()');
  console.log('Endpoints to send to: ', endpoints);
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000, // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
  });
}

Comme webpush.sendNotification() renvoie une promesse, vous pouvez facilement ajouter une gestion des erreurs.

Dans server.js, modifiez à nouveau la fonction sendNotifications:

server.js

function sendNotifications(database, endpoints) {
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000; // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails; // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
    let id = endpoint.substr((endpoint.length - 8), endpoint.length);
    webpush.sendNotification(subscription, notification, options)
    .then(result => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Result: ${result.statusCode} `);
    })
    .catch(error => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Error: ${error.body} `);
    });
  });
}

Gérer les nouveaux abonnements

Contexte

Voici ce qui se passe lorsque l'utilisateur s'abonne aux notifications push:

  1. L'utilisateur clique sur S'abonner pour envoyer des messages push.

  2. Le client utilise la constante VAPID_PUBLIC_KEY (la clé VAPID publique du serveur) pour générer un objet subscription unique et spécifique au serveur. L'objet subscription se présente comme suit:

       {
         "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         "expirationTime": null,
         "keys":
         {
           "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           "auth": "0IyyvUGNJ9RxJc83poo3bA"
         }
       }
    
  3. Le client envoie une requête POST à l'URL /add-subscription en incluant l'abonnement au format JSON concaténé dans le corps.

  4. Le serveur récupère la chaîne subscription dans le corps de la requête POST, l'analyse au format JSON et l'ajoute à la base de données des abonnements.

    La base de données stocke les abonnements en utilisant leurs propres points de terminaison en tant que clé:

    {
      "https://fcm...1234": {
        endpoint: "https://fcm...1234",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...abcd": {
        endpoint: "https://fcm...abcd",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...zxcv": {
        endpoint: "https://fcm...zxcv",
        expirationTime: ...,
        keys: { ... }
      },
    }

Le nouvel abonnement est maintenant disponible sur le serveur pour l'envoi de notifications.

Implémentation

Les requêtes de nouveaux abonnements passent par la route /add-subscription, qui est une URL POST. Un gestionnaire d'itinéraires bouchons s'affiche dans server.js:

server.js

app.post('/add-subscription', (request, response) => {
  // TODO: implement handler for /add-subscription
  console.log('TODO: Implement handler for /add-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

Dans votre implémentation, ce gestionnaire doit:

  • Récupérez le nouvel abonnement à partir du corps de la requête.
  • Accédez à la base de données des abonnements actifs.
  • Ajoutez le nouvel abonnement à la liste des abonnements actifs.

Pour gérer les nouveaux abonnements:

  • Dans server.js, modifiez le gestionnaire de routes pour /add-subscription comme suit:

    server.js

    app.post('/add-subscription', (request, response) => {
      // TODO: implement handler for /add-subscription
      console.log('TODO: Implement handler for /add-subscription');
      console.log('Request body: ', request.body);
      let subscriptions = Object.assign({}, request.session.subscriptions);
      subscriptions[request.body.endpoint] = request.body;
      request.session.subscriptions = subscriptions;
      response.sendStatus(200);
    });

Gérer les résiliations d'abonnements

Contexte

Le serveur ne saura pas toujours quand un abonnement devient inactif. Par exemple, un abonnement peut être effacé à l'arrêt du service worker par le navigateur.

Toutefois, le serveur peut être informé des abonnements résiliés via l'interface utilisateur de l'application. Au cours de cette étape, vous allez implémenter une fonctionnalité permettant de supprimer un abonnement de la base de données.

De cette façon, le serveur évite d'envoyer un grand nombre de notifications à des points de terminaison qui n'existent pas. Évidemment, cela n'a pas vraiment d'importance avec une simple application de test, mais cela devient important à plus grande échelle.

Implémentation

Les requêtes d'annulation d'abonnements sont envoyées à l'URL POST /remove-subscription.

Le gestionnaire de routes bouchon dans server.js se présente comme suit:

server.js

app.post('/remove-subscription', (request, response) => {
  // TODO: implement handler for /remove-subscription
  console.log('TODO: Implement handler for /remove-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

Dans votre implémentation, ce gestionnaire doit:

  • Récupérez le point de terminaison de l'abonnement résilié dans le corps de la requête.
  • Accédez à la base de données des abonnements actifs.
  • Supprimez l'abonnement annulé de la liste des abonnements actifs.

Le corps de la requête POST du client contient le point de terminaison que vous devez supprimer:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}

Pour gérer les résiliations d'abonnement:

  • Dans server.js, modifiez le gestionnaire de routes pour /remove-subscription comme suit:

    server.js

  app.post('/remove-subscription', (request, response) => {
    // TODO: implement handler for /remove-subscription
    console.log('TODO: Implement handler for /remove-subscription');
    console.log('Request body: ', request.body);
    let subscriptions = Object.assign({}, request.session.subscriptions);
    delete subscriptions[request.body.endpoint];
    request.session.subscriptions = subscriptions;
    response.sendStatus(200);
  });