https://a.storyblok.com/f/270183/113226/402c20ead6/build-a-check-in-app_1200x675.jpg

Criação de um aplicativo de check-in com a Verify API da Nexmo e o Koa.js

Publicado em April 26, 2021

Tempo de leitura: 13 minutos

Digamos que você esteja conduzindo um jogo como The Amazing Race, em que há vários pontos de controle que os participantes precisam alcançar fisicamente antes de poderem concluir o jogo. Este aplicativo serve para monitorar se um participante chegou a um ponto de controle ou não.

Um pouco sobre a autenticação de dois fatores (2FA)

Um fluxo típico de autenticação de dois fatores envolve apenas uma parte. Ela digita seu número de celular para receber um SMS com o código de verificação e, em seguida, insere esse código na interface do usuário para autenticar sua identidade. É moleza.

As coisas ficam um pouco mais interessantes se você quiser transformar isso em uma atividade para duas pessoas. O administrador do ponto de controle terá uma lista de todos os jogadores e seus respectivos números de telefone. Quando os jogadores chegarem ao ponto de controle, o administrador gerará um código de verificação que será enviado para o celular do jogador.

O jogador deverá, então, inserir esse código de verificação por meio de um formulário online ou responder por SMS para confirmar sua presença no ponto de controle. Em teoria, como o administrador não tem como acessar o código de verificação, ele não poderá verificar os jogadores que não estiverem presentes no ponto de controle.

Antes que você mencione as inúmeras maneiras pelas quais os jogadores ainda podem agir em conluio com os administradores, deixe-me garantir que estou ciente delas, mas esta é uma versão MVP e vamos retomar a questão da prevenção de fraudes (e uma infinidade de outros recursos) em versões futuras. Talvez.

Bibliotecas utilizadas

A Nexmo Verify API é normalmente usada para autenticação de dois fatores ou para autenticação sem senha. Em vez de desenvolver eu mesmo uma funcionalidade segura de geração de OTP, optei por usá-la para meus códigos de verificação.

Koa.js é a estrutura por trás do aplicativo, responsável pelo serviço, roteamento, tratamento de solicitações e respostas de API, etc. Como a estrutura principal do Koa.js é bastante básica, é necessário adicionar vários middlewares onde for necessário.

Nunjucks é o mecanismo de modelos para renderizar dados no front-end, enquanto lowdb é um banco de dados JSON muito simples, ótimo para protótipos como este aplicativo. Todas as funções relacionadas ao banco de dados podem ser facilmente substituídas por um banco de dados mais “sério”.

Um aplicativo básico em Koa.js no Glitch

Se você já estiver usando Glitch, por favor, pule tudo isso. Para quem ainda não conhece a incrível plataforma que é o Glitch, ao acessá-la pela primeira vez, você pode escolher que tipo de projeto deseja criar. Existem três opções pré-definidas: um site simples (sem back-end), uma aplicação Node e uma aplicação Node com um banco de dados SQLite. Para esta demonstração, você pode escolher a segunda opção.

Starting a new Node project on GlitchStarting a new Node project on Glitch

Para garantir que seu projeto seja salvo, é uma boa ideia criar um Account no Glitch. O Glitch vem aprimorando seus recursos com bastante frequência, então isso pode mudar se você estiver lendo este texto muito tempo depois, mas, no momento em que este texto foi escrito, o serviço oferecia login via Facebook, GitHub, e-mail ou código de login.

Sign in to a Glitch accountSign in to a Glitch account

Por padrão, as aplicações Node no Glitch são executadas no Express, o que é perfeitamente aceitável. Este projeto em particular usa o Koa.js, então há mais algumas etapas a serem seguidas para isso.

Default package.json on a fresh Glitch Node projectDefault package.json on a fresh Glitch Node project

Ao clicar em “Ferramentas”, no canto inferior esquerdo da tela, serão exibidas algumas opções, como “Logs”, “Console”, “Estatísticas do contêiner” e assim por diante.

Tools options on GlitchTools options on Glitch

É ótimo manter a janela de logs aberta durante o desenvolvimento do seu aplicativo, pois tudo o que você console.log() aparece aqui.

Viewing logs on GlitchViewing logs on Glitch

Para personalizar os módulos do npm que você deseja usar em seu projeto, é possível acessar a linha de comando da mesma forma que faria em sua máquina local ou em um servidor remoto. É importante observar que, em vez de npm, o Glitch usa pnpm como gerenciador de pacotes.

Accessing the Glitch consoleAccessing the Glitch console

Remova o Express executando o seguinte:

pnpm uninstall express

Em seguida, instale o Koa.js executando o seguinte comando:

pnpm install koa --save

Para verificar quais módulos do npm estão sendo usados no seu projeto, você precisará atualizar o ambiente:

refresh

Depois de fazer isso, você deverá ver um indicador de “Erro” ao lado de Ferramentas. Isso é normal, pois no server.js arquivo, estamos exigindo o framework Express, que não está mais presente.

O próximo passo é reescrever o código básico do servidor para usar o Koa.js. Você pode fazer isso por conta própria ou colar o código a seguir no arquivo recém-criado.

const Koa = require('koa')
const port = process.env.PORT || 3000
const app = new Koa()

app.use(async ctx => {
  ctx.body = 'Hello Dinosaur 🦖'
})

const listener = app.listen(port, function() {
  console.log('Your app is listening on port ' + listener.address().port)
})

Se tudo correu bem, clicar no botão “Mostrar” na barra de navegação superior deve abrir seu aplicativo em uma nova janela com o texto “Olá, dinossauro 🦖”.

Check that Koa.js is running fineCheck that Koa.js is running fine

A estrutura do aplicativo

Na verdade, deveria haver um gerenciamento adequado de usuários para os administradores. Se você acessar a demonstração ao vivo no Glitch, verá que há uma espécie de proteção por senha, mas trata-se de uma implementação muito rudimentar para esse protótipo simples. O protótipo se limita a apresentar a ideia de como a interface funcionaria.

Isso significa que o aplicativo terá três páginas: a página de login, a página do administrador e a página de inserção do código de verificação.

Rough screen sketches for login page, administrator page and verification code entry pageRough screen sketches for login page, administrator page and verification code entry page

Conforme mencionado anteriormente, é necessário instalar alguns middlewares adicionais. Este projeto utiliza os seguintes:

  • koa-static para servir recursos estáticos

  • koa-bodyparser para processar dados enviados por meio de solicitações POST

  • koa-router para roteamento

  • koa-views para renderizar modelos do Nunjucks (também requer que o Nunjucks esteja instalado)

  • koa-session para proteção básica por senha (não para ambiente de produção)

Exibição de recursos estáticos

const serve = require('koa-static')
app.use(serve('./public'))

Essa provavelmente será a parte menos complicada de abordar: o fornecimento de recursos estáticos, como CSS e JavaScript do lado do cliente, a partir da pasta pasta /public .

Roteamento e renderização básicos

O plano é fazer com que tudo funcione sem nenhum JavaScript do lado do cliente. Assim, as entradas do usuário são enviadas por meio de formulários HTML e, se necessário, ocorre um redirecionamento para a página apropriada após o envio. Cada página é um arquivo HTML independente e é renderizada com koa-views, que fornece uma render() função.

const Router = require('koa-router')
const views = require('koa-views')
const router = new Router()

app.use(views('./views', { map: { html: 'nunjucks' }}))

router.get('/login', (ctx, next) => {
  return ctx.render('./login')
})

router.get('/', (ctx, next) => {
  return ctx.render('./index')
})

router.get('/verify/:phone', (ctx, next) => {
  const phone = ctx.params.phone
  return ctx.render('./verify')
})

router.get('/result/:phone', (ctx, next) => {
  const phone = ctx.params.phone
  return ctx.render('./result')
})

koa-router também oferece uma maneira de acessar parâmetros de URL por meio de ctx.params, usado para */Verify/* e /result/**, que é usado para associar números de telefone ao código de verificação gerado. Isso ficará mais claro quando explicarmos como a Verify API funciona.

Verify API

A utilização da Verify API envolve duas etapas. A primeira consiste em enviar um SMS contendo a senha de uso único (OTP) para o número de telefone do destinatário.

nexmo.verify.request({
  number: RECIPIENT_NUMBER,
  brand: NEXMO_BRAND_NAME
}, (err, result) => {
  if (err) {
    console.error(err)
  } else {
    const verifyRequestId = result.request_id
    console.log('request_id', verifyRequestId)
  }
})

A resposta HTTP da API de solicitação de Verify tem o seguinte formato:

{
  "request_id": "aaaaaaaa-bbbb-...",
  "status": "0",
  "error_text": "error"
}

Se o status for diferente de 0, a solicitação não foi bem-sucedida e os detalhes sobre o motivo podem ser encontrados em error_text. Caso contrário, o destinatário deve receber uma OTP por SMS. Ele deverá, então, inserir essa OTP no seu aplicativo, que poderá ser verificada em relação à request_id.

nexmo.verify.check({
  request_id: REQUEST_ID,
  code: CODE
}, (err, result) => {
  if (err) {
    console.error(err)
  } else {
    console.log(result)
  }
})

A resposta HTTP da Verify API tem o seguinte formato:

{
  "request_id": "aaaaaaaa-bbbb-...",
  "event_id": "0A00000012345678",
  "status": "0",
  "price": "0.10000000",
  "currency": "EUR",
  "error_text": "error"
}

Como lidar com vários jogadores

O administrador do ponto de controle precisa acompanhar todos os jogadores; portanto, pode ser uma boa ideia usar um banco de dados para armazenar essas informações. Esta demonstração utiliza lowdb, que é um pequeno banco de dados JSON local baseado no Lodash, mas você tem total liberdade para usar qualquer banco de dados que preferir.

Existem várias funções relacionadas a bancos de dados para adicionar jogadores, recuperar informações sobre jogadores e atualizar essas informações. Ao organizar as funções dessa maneira, fica mais fácil trocar de banco de dados, pois a lógica permanece a mesma independentemente do banco de dados utilizado.

function dbAddPlayer(data) {
  db.get('players')
    .push({ name: data.name, phone: data.phone })
    .write()
  console.log('New user inserted in the database')
}

function dbGetPlayers() {
  return db.get('players').value()
}

function dbPlayerCount() {
  return db.get('players').size().value()
}

function dbAddId(phone, requestId, mode, status) {
  db.get('players')
    .find({ phone: phone })
    .assign({ id: requestId, delivery: mode, status: status })
    .write()
}

function dbUpdateStatus(requestId, status) {
  db.get('players')
    .find({ id: requestId })
    .assign({ status: status })
    .write()
}

function dbFindPlayer(phone) {
  return db.get('players').find({ phone: phone }).value()
}

function dbClear() {
  db.get('players')
    .remove()
    .write()
  console.log('Database cleared')
}

Você pode usar um formulário da web para coletar as informações necessárias para criar um novo jogador. Para esta demonstração, apenas dois campos são obrigatórios: o nome do jogador e seu número de telefone.

<form id="addPlayerForm" action="add" method="post">
  <h2>Add player</h2>
  <div class="inputs">
    <label>
      <span>Name</span>
      <input name="name" required>
    </label>
    <label>
      <span>Phone</span>
      <input type="tel" name="phone" required>
    </label>
  </div>
  <button id="addPlayer">Add</button>
</form>

Ao enviar este formulário, será enviada uma solicitação POST para /add, o que significa que você precisa criar uma rota para processar esses dados recebidos e, em seguida, armazená-los no banco de dados.

router.post('/add', (ctx, next) => {
  const payload = ctx.request.body
  dbAddPlayer(payload)
  ctx.status = 200
  ctx.response.redirect('/')
})

Você pode, então, exibir informações do banco de dados na página usando o Nunjucks. O Nunjucks oferece uma render() função que permite passar dados para o seu modelo do Nunjucks. Modifique a GET rota para / para que você possa exibir as informações do jogador do banco de dados na página.

router.get('/', (ctx, next) => {
  const players = dbGetPlayers()
  return ctx.render('./index', { players: players })
})

Em seguida, você pode inserir os valores dos jogadores no modelo e exibi-los da maneira que desejar que os dados dos jogadores sejam estruturados.

Displaying player data on the frontendDisplaying player data on the frontend

Ativação da OTP

A Verify API exige o número de telefone do jogador para acionar a solicitação da OTP. Você pode enviar essa informação ao backend por meio do envio de outro formulário da web, desta vez enviando para */verify/{{ player.phone }}*, o que permite que você obtenha o número de telefone por meio de ctx.params.phone e repassá-lo à função de solicitação da Verify API.

router.post('/verify/:phone', async (ctx, next) => {
  const phone = ctx.params.phone
  const result = await verify(phone)
  dbAddId(phone, result.request_id, payload.delivery, 'pending')
  ctx.status = 200
  ctx.response.redirect('/')
})

// Verify API's request function
async function verify(number) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.request({
      number: number,
      brand: process.env.NEXMO_BRAND_NAME
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

// Add request ID to the database
function dbAddId(phone, requestId, mode, status) {
  db.get('players')
    .find({ phone: phone })
    .assign({ id: requestId, delivery: mode, status: status })
    .write()
}

O request_id, que faz parte da resposta da API, será necessário para a função subsequente check() . Essa função será usada para verificar o request_id em relação ao código inserido pelo jogador. Isso é armazenado no banco de dados por meio da dbAddId() função.

Trigger the OTP for each phone numberTrigger the OTP for each phone number

Verificação do OTP

Os jogadores podem inserir o código de verificação na URL exclusiva, */verify/{{ player.phone }}*. Você precisaria passar o número de telefone de volta para a página para que sua GET rota fique semelhante à de /, que inclui a render() função.

router.get('/verify/:phone', (ctx, next) => {
  const phone = ctx.params.phone
  return ctx.render('./verify', { phone: phone })
})

O formulário da web para os jogadores inserirem sua OTP terá uma aparência mais ou menos assim:

<form method="post" action="/check" id="checkPinForm">
  <input name="pin" type="number">
  <input type="hidden" name="phone" value="{{ phone }}">
  <button>Submit</button>
</form>

Web form for entering the OTP sent to players' phonesWeb form for entering the OTP sent to players' phones

Quando o usuário inserir a OTP, você poderá recuperar a request_id do banco de dados e passar tanto o request_id , quanto o PIN para a função da Verify API check() .

router.post('/check', async (ctx, next) => {
  const payload = await ctx.request.body
  const phone = payload.phone
  const code = payload.pin
  
  const requestId = dbFindPlayer(phone).id

  const result = await check(requestId, code)
  dbUpdateStatus(requestId, result.status)
  
  ctx.status = 200
  ctx.response.redirect('/result/' + payload.phone)
})

// Verify API's check function
async function check(requestId, code) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.check({
      request_id: requestId,
      code: code
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

// Update player status in the database
function dbUpdateStatus(requestId, status) {
  db.get('players')
    .find({ id: requestId })
    .assign({ status: status })
    .write()
}

Uma verificação bem-sucedida retornará um código de status de 0, que você pode usar para determinar qual mensagem de status exibir ao usuário na página de resultados.

<main>{% raw %}
  {% if status == 0 %}
  <p>Code verified successfully.</p>
  <p>You can close this window now.</p>
  {% else %}
  <p>Something went wrong…</p>
  <p>Please contact the administrator for more information.</p>
  {% endif %}{% endraw %}
</main>

Verification results page displayed to the userVerification results page displayed to the user

Outras coisas que você pode fazer

A API de mensagens da Nexmo Messages API da Nexmo também pode ser usada para adicionar funcionalidades a este projeto. Você poderia, por exemplo, enviar um SMS adicional contendo o link para a página de verificação, a fim de proporcionar uma melhor experiência ao usuário.

Ou talvez o seu jogo se passe em um local bastante remoto, como, por exemplo, Pulau Perhentian (foto abaixo), ou, por algum motivo, seus jogadores não têm dados móveis. Assim, em vez de verificar o código por meio de uma interface web, os jogadores podem responder por SMS.

 Pulau Perhentian Pulau Perhentian

A versão hospedada em Glitch abrange os dois cenários acima, então fique à vontade para conferir e fazer um remix.

remix this

Este protótipo é uma demonstração do que é possível fazer com a Verify API, mas, mais uma vez, para que isso se torne um aplicativo completo, há vários aspectos que precisam ser resolvidos. Um dos mais importantes é o tratamento de erros. A Verify API retorna um valor de status de 0 para consultas bem-sucedidas, mas qualquer outro valor indica um erro.

Esses erros devem ser tratados, e a interface do usuário no front-end deve refletir quaisquer erros potenciais que impeçam a verificação bem-sucedida. Também pode ser uma boa ideia implementar algum tipo de validação no front-end ou até mesmo utilizar a Number Insight API para garantir que apenas números de telefone válidos sejam enviados à Verify API.

E agora, para onde vamos?

Se você quiser explorar mais essas APIs, aqui estão alguns links que podem ser úteis:

Compartilhar:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing ChenEx-funcionários da Vonage

Hui Jing é Developer Advocate na Nexmo. Ela tem um amor imenso por CSS e tipografia e, de modo geral, é apaixonada por tudo que diz respeito à web.