Criar um servidor de notificações push

Neste codelab, você vai criar um servidor de notificações push. O servidor gerenciará uma lista de inscrições push e enviará notificações para elas.

O código do cliente já está completo. Neste codelab, você vai trabalhar na funcionalidade do lado do servidor.

Remixar o app de exemplo e abrir em uma nova guia.

As notificações são bloqueadas automaticamente no app Glitch incorporado. Por isso, não será possível visualizar o app nesta página. Em vez disso, faça o seguinte:

  1. Clique em Remixar para editar para tornar o projeto editável.
  2. Para visualizar o site, pressione Ver app. Em seguida, pressione Tela cheia modo tela cheia.

O app ativo é aberto em uma nova guia do Chrome. No Glitch incorporado, clique em View Source para mostrar o código novamente.

Enquanto você trabalha neste codelab, faça mudanças no código no Glitch incorporado desta página. Atualize a nova guia com o app ativo para conferir as mudanças.

Conhecer o app inicial e o código dele

Comece analisando a interface do cliente do app.

Na nova guia do Chrome:

  1. Pressione "Control + Shift + J" (ou "Command + Option + J" no Mac) para abrir o DevTools. Clique na guia Console.

  2. Clique nos botões na interface (verifique a saída no Console do desenvolvedor do Chrome).

    • Registrar service worker registra um service worker no escopo do URL do projeto do Glitch. Cancelar registro do service worker remove o service worker. Se houver uma assinatura de push anexada, ela também será desativada.

    • Inscrever-se para receber push cria uma assinatura de push. Ela só estará disponível quando um service worker tiver sido registrado e uma constante VAPID_PUBLIC_KEY estiver presente no código do cliente (vamos saber mais sobre isso depois), então ainda não é possível clicar nela.

    • Quando você tem uma assinatura de push ativa, a opção Notificar a assinatura atual solicita que o servidor envie uma notificação para o endpoint.

    • Notificar todas as assinaturas pede que o servidor envie uma notificação a todos os endpoints de assinatura no banco de dados.

      Alguns desses endpoints podem estar inativos. É possível que uma assinatura desapareça quando o servidor enviar uma notificação a ela.

Vamos descobrir o que está acontecendo no servidor. Para conferir as mensagens do código do servidor, consulte o registro do Node.js na interface do Glitch.

  • No app Glitch, clique em Tools -> Logs.

    Provavelmente, você verá uma mensagem como Listening on port 3000.

    Se você tentou clicar em Notificar assinatura atual ou Notificar todas as assinaturas na interface do app ativo, também verá a seguinte mensagem:

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

Agora, vamos conferir alguns códigos.

  • public/index.js contém o código do cliente concluído. Ele realiza detecção de recursos, registra e cancela o registro do service worker e controla a assinatura do usuário para notificações push. Ele também envia ao servidor informações sobre assinaturas novas e excluídas.

    Como você trabalhará apenas na funcionalidade do servidor, você não editará esse arquivo (além de preencher a constante VAPID_PUBLIC_KEY).

  • public/service-worker.js é um service worker simples que captura eventos push e mostra notificações.

  • /views/index.html contém a interface do app.

  • O .env contém as variáveis de ambiente que o Glitch carrega no servidor do app quando é iniciado. Você preencherá .env com detalhes de autenticação para enviar notificações.

  • server.js é o arquivo em que você fará a maior parte do trabalho neste codelab.

    O código inicial cria um servidor da Web Express simples. Há quatro itens TODO para você, marcados nos comentários de código com TODO:. Você vai precisar:

    Neste codelab, você vai trabalhar com esses itens, um de cada vez.

Gerar e carregar detalhes VAPID

Seu primeiro item TODO é gerar detalhes VAPID, adicioná-los às variáveis de ambiente Node.js e atualizar o código do cliente e do servidor com os novos valores.

Contexto

Quando os usuários se inscrevem para receber notificações, eles precisam confiar na identidade do app e do servidor. Os usuários também precisam ter certeza de que, quando recebem uma notificação, elas são do mesmo app que configurou a assinatura. Eles também precisam confiar que ninguém mais pode ler o conteúdo da notificação.

O protocolo que torna as notificações push seguras e privadas é chamado de Identificação Voluntária de Servidor de Aplicativo para Push na Web (VAPID, na sigla em inglês). A VAPID usa a criptografia de chave pública para verificar a identidade de apps, servidores e endpoints de assinatura, além de criptografar o conteúdo de notificações.

Neste app, você vai usar o pacote npm do push da Web (link em inglês) para gerar chaves VAPID, além de criptografar e enviar notificações.

Implementação

Nesta etapa, gere um par de chaves VAPID para seu app e adicione-as às variáveis de ambiente. Carregue as variáveis de ambiente no servidor e adicione a chave pública como uma constante no código do cliente.

  1. Use a função generateVAPIDKeys da biblioteca web-push para criar um par de chaves VAPID.

    No server.js, remova os comentários ao redor das seguintes linhas de código:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  2. Depois que o Glitch reinicia o app, ele gera as chaves para o registro do Node.js na interface do Glitch, e não no Console do Chrome. Para conferir as chaves VAPID, selecione Tools -> Logs na interface do Glitch.

    Copie suas chaves pública e privada do mesmo par de chaves.

    O Glitch reinicia o app sempre que você edita o código. Assim, o primeiro par de chaves gerado pode rolar para fora da visualização conforme mais saídas vêm a seguir.

  3. Em .env, copie e cole as chaves VAPID. Coloque as chaves entre aspas duplas ("...").

    Para VAPID_SUBJECT, você pode inserir "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. Em server.js, comente essas duas linhas de código novamente, já que você só precisa gerar as chaves VAPID uma vez.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  5. No server.js, carregue os detalhes do VAPID das variáveis de ambiente.

    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. Copie e cole também a chave public no código do cliente.

    Em public/index.js, insira o mesmo valor de VAPID_PUBLIC_KEY que você copiou no arquivo .env:

    public/index.js

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

Implementar a funcionalidade para enviar notificações

Contexto

Neste app, você vai usar o pacote npm do push da Web (link em inglês) para enviar notificações.

Este pacote criptografa automaticamente as notificações quando webpush.sendNotification() é chamado. Então, você não precisa se preocupar com isso.

O web-push aceita várias opções de notificações. Por exemplo, você pode anexar cabeçalhos à mensagem e especificar a codificação de conteúdo.

Neste codelab, você usará apenas duas opções, definidas com as seguintes linhas de código:

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

A opção TTL (vida útil) define um tempo limite de expiração em uma notificação. Essa é uma forma do servidor evitar enviar uma notificação a um usuário quando ela não for mais relevante.

A opção vapidDetails contém as chaves VAPID que você carregou das variáveis de ambiente.

Implementação

Em server.js, modifique a função sendNotifications da seguinte maneira:

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);
  });
}

Como webpush.sendNotification() retorna uma promessa, é possível adicionar o tratamento de erros facilmente.

Em server.js, modifique a função sendNotifications novamente:

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} `);
    });
  });
}

Processar novas assinaturas

Contexto

Veja o que acontece quando o usuário se inscreve para receber notificações push:

  1. O usuário clica em Inscrever-se para receber push.

  2. O cliente usa a constante VAPID_PUBLIC_KEY (a chave VAPID pública do servidor) para gerar um objeto subscription exclusivo e específico do servidor. O objeto subscription tem esta aparência:

       {
         "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         "expirationTime": null,
         "keys":
         {
           "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           "auth": "0IyyvUGNJ9RxJc83poo3bA"
         }
       }
    
  3. O cliente envia uma solicitação POST para o URL /add-subscription, incluindo a assinatura como JSON stringificado no corpo.

  4. O servidor recupera o subscription em formato de string do corpo da solicitação POST, analisa-o de volta para JSON e o adiciona ao banco de dados de assinaturas.

    As assinaturas são armazenadas usando os próprios endpoints como uma chave:

    {
      "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: { ... }
      },
    }

Agora, a nova assinatura está disponível para que o servidor envie notificações.

Implementação

As solicitações de novas assinaturas vêm para o trajeto /add-subscription, que é um URL POST. Você verá um gerenciador de rota de stub em 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);
});

Na sua implementação, esse gerenciador precisa:

  • Recupere a nova assinatura no corpo da solicitação.
  • Acessar o banco de dados de assinaturas ativas.
  • Adicione a nova assinatura à lista de assinaturas ativas.

Para gerenciar novas assinaturas, faça o seguinte:

  • No server.js, modifique o gerenciador de rota para /add-subscription da seguinte maneira:

    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);
    });

Processar cancelamentos de assinaturas

Contexto

O servidor nem sempre sabe quando uma assinatura se torna inativa. Por exemplo, uma assinatura pode ser apagada quando o navegador encerra o service worker.

No entanto, o servidor pode saber mais sobre assinaturas canceladas pela interface do app. Nesta etapa, você vai implementar uma funcionalidade para remover uma assinatura do banco de dados.

Dessa forma, o servidor evita o envio de várias notificações para endpoints inexistentes. Obviamente, isso não importa com um simples aplicativo de teste, mas se torna importante em uma escala maior.

Implementação

As solicitações de cancelamento de assinaturas chegam ao URL POST /remove-subscription.

O gerenciador de rota de stub no server.js tem a seguinte aparência:

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);
});

Na sua implementação, esse gerenciador precisa:

  • Extraia o endpoint da assinatura cancelada do corpo da solicitação.
  • Acessar o banco de dados de assinaturas ativas.
  • Remove a assinatura cancelada da lista de assinaturas ativas.

O corpo da solicitação POST do cliente contém o endpoint que você precisa remover:

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

Para processar cancelamentos de assinaturas, faça o seguinte:

  • No server.js, modifique o gerenciador de rota para /remove-subscription da seguinte maneira:

    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);
  });