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 une liste d'abonnements push et leur enverra des notifications.

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

Remixer l'application exemple et l'afficher dans un nouvel onglet

Les notifications sont automatiquement bloquées dans l'application Glitch intégrée. Vous ne pourrez donc pas prévisualiser l'application sur cette page. Voici comment procéder:

  1. Cliquez sur Remix to Edit (Remixer pour modifier) pour rendre le projet modifiable.
  2. Pour prévisualiser le site, appuyez sur Afficher l'application, puis 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é présenté 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 des résultats).

    • L'option Register service worker (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 du service worker supprime le service worker. Si un abonnement push lui est associé, il sera également désactivé.

    • L'option S'abonner au service 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 y reviendrons plus tard). Vous ne pouvez donc pas cliquer dessus pour l'instant.

    • Lorsque vous disposez d'un abonnement push actif, la fonction Notifier l'abonnement actuel demande que le serveur envoie une notification à son point de terminaison.

    • L'option 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 toujours possible qu'un abonnement disparaisse lorsque le serveur lui envoie une notification.

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

  • Dans l'application Glitch, cliquez sur Tools -> Logs (Outils -> Journaux).

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

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

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

Examinons maintenant quelques lignes de code.

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

    Étant donné que vous allez uniquement travailler sur la fonctionnalité serveur, vous n'allez pas modifier ce fichier (vous devrez remplir la constante VAPID_PUBLIC_KEY).

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

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

  • .env contient les variables d'environnement que Glitch charge dans votre serveur d'applications au démarrage. Vous allez renseigner .env avec les informations d'authentification pour envoyer des notifications.

  • server.js est le fichier dans lequel vous allez effectuer la majeure partie de votre travail au cours de cet atelier de programmation.

    Le code de démarrage crée un serveur Web Express simple. Vous avez quatre éléments de tâche à effectuer, marqués de TODO: dans les commentaires du code. Vos tâches sont les suivantes :

    Dans cet atelier de programmation, vous allez effectuer les tâches suivantes un par un.

Générer et charger des informations VAPID

La première étape consiste à générer des informations VAPID, à les ajouter aux variables d'environnement Node.js, puis à mettre à jour le code client et serveur avec les nouvelles valeurs.

Contexte

Lorsque les utilisateurs s'abonnent aux notifications, ils doivent pouvoir faire confiance à l'identité de l'application et de son serveur. Les utilisateurs doivent également s'assurer que lorsqu'ils reçoivent une notification, elle 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). Le VAPID utilise la cryptographie à clé publique pour valider l'identité des applications, des serveurs et des points de terminaison des abonnements, et pour chiffrer le contenu des notifications.

Dans cette application, vous allez utiliser le package web-push npm pour générer des clés VAPID, 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 enregistré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 -> Logs (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, il est possible que la première paire de clés générée ne soit plus visible, tandis que d'autres résultats sont générés par la suite.

  3. Dans le fichier .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 à nouveau ces deux lignes de code en commentaire, 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 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é publique 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 web-push npm pour envoyer des notifications.

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

web-push accepte plusieurs options de notification, par exemple vous pouvez 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 (durée de vie) définit un délai d'expiration pour une notification. Cela permet au serveur d'éviter d'envoyer une notification à un utilisateur lorsque celle-ci 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 au service push.

  2. Le client utilise la constante VAPID_PUBLIC_KEY (la clé VAPID publique du serveur) pour générer un objet subscription unique, 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 sous forme de fichier 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 "subscriptions".

    La base de données stocke les abonnements en utilisant leurs propres points de terminaison comme 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: { ... }
      },
    }

Désormais, le nouvel abonnement est disponible sur le serveur pour l'envoi de notifications.

Implémentation

Les requêtes de nouveaux abonnements sont transmises par la route /add-subscription, qui est une URL POST. Un gestionnaire de routage bouchon 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 dans le 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 annulations d'abonnement

Contexte

Le serveur ne saura pas toujours quand un abonnement devient inactif. Par exemple, les données d'un abonnement peuvent être effacées lorsque le navigateur arrête le service worker.

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 inexistants. Évidemment, cela n'a pas vraiment d'importance avec une application de test simple, mais cela devient important à plus grande échelle.

Implémentation

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

Le gestionnaire de routage des bouchons 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);
  });