Criar um servidor de notificações push

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

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

As notificações são bloqueadas automaticamente no app Glitch incorporado, então não é 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 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.

Durante este codelab, faça mudanças no código no Glitch incorporado nesta página. Atualize a nova guia com o app ativo para conferir as mudanças.

Conhecer o app inicial e o código dele

Para começar, analise 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. Tente clicar em botões na interface do usuário (verifique o console do desenvolvedor do Chrome para saber o resultado).

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

    • Inscrever-se para push cria uma assinatura de push. Ela só fica disponível depois que um service worker é registrado e uma constante VAPID_PUBLIC_KEY está presente no código do cliente (falaremos sobre isso mais adiante). Por isso, 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 diz ao servidor para enviar uma notificação para todos os endpoints de assinatura no banco de dados.

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

Vamos ver o que acontece no lado do servidor. Para ver as mensagens do código do servidor, consulte o registro do Node.js na interface do Glitch.

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

    Você provavelmente verá uma mensagem como Listening on port 3000.

    Se você tentou clicar em Notificar assinatura atual ou Notificar todas as inscrições na interface do app publicado, esta mensagem também será exibida:

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

Agora vamos analisar o código.

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

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

  • O public/service-worker.js é um service worker simples que captura eventos push e exibe 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ê vai preencher .env com detalhes de autenticação para enviar notificações.

  • server.js é o arquivo em que você fará a maior parte do trabalho durante este 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 TODO, um de cada vez.

Gerar e carregar detalhes VAPID

Seu primeiro item TODO é gerar detalhes do 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 aplicativo e do servidor. Os usuários também precisam ter certeza de que, quando recebem uma notificação, são do mesmo app que configurou a assinatura. Eles também precisam confiar que ninguém mais poderá ler o conteúdo das notificações.

O protocolo que torna as notificações push seguras e particulares é chamado de Identificação voluntária de servidores de aplicativos 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 web-push 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.

    Em server.js, remova os comentários 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 geradas para o registro do Node.js na interface do Glitch, e não no console do Chrome. Para acessar as teclas VAPID, selecione Ferramentas -> 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. Por isso, o primeiro par de chaves gerado pode sair da visualização conforme mais resultados mostrados 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, marque 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, digite 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 web-push para enviar notificações.

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

O web-push aceita várias opções de notificações, por exemplo, é possível anexar cabeçalhos à mensagem e especificar a codificação do conteúdo.

Neste codelab, você usará apenas duas opções, definidas pelas 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 que uma notificação seja enviada a um usuário quando ela não for mais relevante.

A opção vapidDetails contém as chaves VAPID que você carregou usando as 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, você pode adicionar processamento 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 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 vai ficar assim:

       {
         "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 em string no corpo.

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

    O banco de dados armazena assinaturas usando os próprios endpoints como 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 chegam à rota /add-subscription, que é um URL POST. Você verá um gerenciador de rotas de stub no 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 lidar com novas assinaturas:

  • Em server.js, modifique o gerenciador de rotas 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 descobrir assinaturas que foram 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 um monte de notificações para endpoints inexistentes. Obviamente, isso não importa com um simples app de teste, mas se torna importante em uma escala maior.

Implementação

Os pedidos de cancelamento de assinaturas são enviados para o URL POST /remove-subscription.

O gerenciador de rotas 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:

  • Recupere o endpoint da assinatura cancelada do corpo da solicitação.
  • Acessar o banco de dados de assinaturas ativas.
  • Remova 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 lidar com cancelamentos de assinaturas:

  • Em server.js, modifique o gerenciador de rotas 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);
  });