Criar um servidor de notificações push

Neste codelab, você vai 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á completo. 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 Remix to Edit para tornar o projeto editável.
  2. Para visualizar o site, pressione View App. Em seguida, pressione Fullscreen tela cheia.

O app ao vivo é 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 em tempo real 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 as Ferramentas do desenvolvedor. Clique na guia Console.

  2. Tente clicar nos botões da interface (confira a saída no console do desenvolvedor do Chrome).

    • Register service worker registra um service worker para o escopo do URL do projeto do Glitch. Cancelar inscrição do service worker remove o service worker. Se uma assinatura push estiver anexada, ela também será desativada.

    • Inscrever-se em push cria uma assinatura de push. Ela só fica disponível quando 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 push ativa, a ação Notificar a assinatura atual solicita que o servidor envie uma notificação para o endpoint.

    • Notificar todas as assinaturas instrui o servidor a 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 assinatura desapareça no momento em que o servidor envia uma notificação para 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 -> Logs.

    Você provavelmente vai receber uma mensagem como Listening on port 3000.

    Se você tentou clicar em Notificar assinatura atual ou Notificar todas as assinaturas na interface do app em tempo real, também vai aparecer a seguinte mensagem:

    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 desregistra o worker de serviço e controla a assinatura do usuário para notificações push. Ele também envia informações sobre assinaturas novas e excluídas para o servidor.

    Como você vai trabalhar apenas na funcionalidade do servidor, não vai editar esse arquivo (exceto para preencher a constante VAPID_PUBLIC_KEY).

  • 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 ele é iniciado. Você vai preencher .env com detalhes de autenticação para enviar notificações.

  • server.js é o arquivo em que você vai fazer a maior parte do seu 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 cada item da lista de tarefas a fazer.

Gerar e carregar detalhes VAPID

O primeiro item "TODO" é gerar detalhes do VAPID, adicioná-los às variáveis de ambiente do 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 receberem uma notificação, ela será 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 particulares é chamado de Identificação voluntária de servidores de aplicativos para push na Web (VAPID, na sigla em inglês). O VAPID usa criptografia de chave pública para verificar a identidade de apps, servidores e endpoints de assinatura e criptografar o conteúdo da notificação.

Neste app, você vai usar o pacote npm web-push para gerar chaves VAPID e criptografar e enviar notificações.

Implementação

Nesta etapa, gere um par de chaves VAPID para o 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 e as envia para o registro do Node.js na interface do Glitch (não para o 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 toda vez que você edita o código. Portanto, o primeiro par de chaves gerado pode desaparecer à medida que mais saídas são exibidas.

  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 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. Em 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 a chave pública no código do cliente também.

    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ção. Por exemplo, é possível anexar cabeçalhos à mensagem e especificar a codificação do conteúdo.

Neste codelab, você vai 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 (time to live) define um tempo limite de expiração em uma notificação. Essa é uma forma de o servidor evitar o envio de uma notificação para 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 facilmente o tratamento de erros.

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

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

  1. O usuário clica em Assinar 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 com string no corpo.

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

    O banco de dados armazena as 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 o servidor para enviar notificações.

Implementação

As solicitações de novas assinaturas chegam à rota /add-subscription, que é um URL POST. Um manipulador de rota de rascunho vai aparecer 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:

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

Para processar 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 assinatura

Contexto

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

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

Dessa forma, o servidor evita enviar várias 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 rota de rascunho em server.js tem esta 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.
  • 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 processar 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);
 
});