https://d226lax1qjow5r.cloudfront.net/blog/blogposts/building-a-check-in-app-with-nexmos-verify-api-dr/Build-a-Check-In-App_1200x675.jpg

Construire une application de check-in avec l'API Verify de Nexmo et Koa.js

Publié le April 26, 2021

Temps de lecture : 13 minutes

Imaginons que vous gériez un jeu tel que The Amazing Raceoù il y a plusieurs points de contrôle que les joueurs doivent physiquement atteindre avant de pouvoir terminer le jeu. Cette application permet de savoir si un joueur a atteint un point de contrôle ou non.

Quelques mots sur 2FA

Un flux typique d'authentification à deux facteurs n'implique qu'une seule partie. Elle saisit son numéro de téléphone portable pour recevoir un SMS contenant le code de vérification, puis saisit ce code dans l'interface utilisateur pour authentifier son identité. C'est facile, facile, facile, facile, facile.

Les choses deviennent un peu plus intéressantes si l'on veut que l'affaire se fasse à deux. Un administrateur de point de contrôle disposera d'une liste de tous les joueurs et de leurs numéros de téléphone correspondants. Lorsque les joueurs atteignent le point de contrôle, l'administrateur déclenche un code de vérification qui est envoyé sur le téléphone du joueur.

Le joueur saisit alors ce code de vérification via un formulaire en ligne ou répond par SMS pour confirmer sa présence au point de contrôle. En théorie, comme l'administrateur n'a aucun moyen d'accéder au code de vérification, il ne pourra pas vérifier les joueurs qui ne sont pas présents au point de contrôle.

Avant que vous n'évoquiez la multitude de façons dont les joueurs peuvent encore s'entendre avec les administrateurs, laissez-moi vous assurer que j'en suis conscient, mais il s'agit d'un MVP et nous reviendrons sur la prévention de la fraude (et une myriade d'autres fonctionnalités) dans les prochaines versions. Peut-être.

Bibliothèques utilisées

Nexmo's Verify API de Nexmo de Nexmo est généralement utilisée pour l'authentification à deux facteurs, ou pour l'authentification sans mot de passe. Plutôt que d'écrire moi-même une fonctionnalité sécurisée de génération d'OTP, j'ai choisi de l'utiliser pour mes codes de vérification.

Koa.js est le cadre derrière l'application, pour le service, le routage, la gestion des demandes et des réponses de l'API, etc. Comme le cadre de base de Koa.js est plutôt dépouillé, divers logiciels intermédiaires doivent être ajoutés là où c'est nécessaire.

Nunjucks est le moteur de templating pour le rendu des données sur le frontend, tandis que lowdb est une base de données JSON très simple, idéale pour les prototypes comme cette application. Toutes les fonctions liées à la base de données peuvent être facilement remplacées par une autre base de données plus "sérieuse".

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Une application Koa.js de base sur Glitch

Si vous utilisez déjà Glitchveuillez sauter tout ceci. Pour ceux qui n'ont pas encore découvert l'incroyable plateforme qu'est Glitch, lorsque vous atterrissez pour la première fois, vous pouvez choisir le type de projet que vous souhaitez construire. Il y a 3 préréglages, un simple site web (pas de backend), une application Node et une application Node avec une base de données SQlite. Pour cette démo, vous pouvez choisir la deuxième option.

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

Pour s'assurer que votre projet persiste, il est conseillé de créer un compte Glitch. Glitch a amélioré ses fonctionnalités assez fréquemment, donc cela peut changer si vous lisez dans le futur, mais au moment où j'écris ces lignes, ils prennent en charge la connexion via Facebook, GitHub, email ou code de connexion.

Sign in to a Glitch accountSign in to a Glitch account

Par défaut, les applications Node sur Glitch s'exécutent sur Express, ce qui est tout à fait correct. Ce projet particulier utilise Koa.js, il y a donc quelques étapes supplémentaires à franchir pour cela.

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

En cliquant sur Outils en bas à gauche de l'écran, vous verrez apparaître quelques options, telles que Journaux, Console, Statistiques sur les conteneurs, etc.

Tools options on GlitchTools options on Glitch

Il est très utile d'ouvrir la section Logs lorsque vous développez votre application, car tout ce que vous faites apparaît ici. console.log() s'affiche ici.

Viewing logs on GlitchViewing logs on Glitch

Pour personnaliser les modules npm que vous souhaitez utiliser dans votre projet, vous pouvez accéder à la ligne de commande comme vous le feriez sur votre machine locale ou sur un serveur distant. Une chose à noter est qu'au lieu de npmGlitch utilise pnpm comme gestionnaire de paquets.

Accessing the Glitch consoleAccessing the Glitch console

Supprimez express en exécutant ce qui suit :

pnpm uninstall express

Ensuite, installez Koa.js en exécutant ce qui suit :

pnpm install koa --save

Pour vérifier les modules npm utilisés dans votre projet, vous devrez rafraîchir l'environnement :

refresh

Une fois que vous avez fait cela, vous devriez voir un indicateur "Erreur" à côté de Outils. C'est normal car dans le fichier server.js nous avons besoin du framework Express qui n'existe plus.

La prochaine chose à faire est de réécrire le code de base du serveur pour utiliser Koa.js. Vous pouvez le faire vous-même ou coller le code suivant dans votre fichier nouvellement créé.

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

Si tout s'est bien passé, un clic sur le bouton Afficher de la barre de navigation supérieure devrait déclencher l'affichage de votre application dans une nouvelle fenêtre avec le texte "Hello Dinosaur 🦖".

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

La structure de l'application

En principe, il devrait y avoir une gestion appropriée des utilisateurs pour les administrateurs. Si vous accédez à la démo en direct sur Glitch, vous verrez qu'il y a un semblant de protection par mot de passe, mais il s'agit d'une mise en œuvre très naïve pour ce simple prototype. Le prototype présente simplement l'idée de la façon dont l'interface fonctionnerait.

Cela signifie que l'application aura 3 pages, la page de connexion, la page de l'administrateur, la page de saisie du code de vérification.

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

Comme indiqué précédemment, certains logiciels intermédiaires supplémentaires doivent être installés. Ce projet utilise les éléments suivants :

  • koa-static pour servir des fichiers statiques

  • koa-bodyparser pour traiter les données envoyées via des requêtes POST

  • koa-router pour le routage

  • koa-views pour le rendu des modèles nunjucks (nécessite également l'installation de nunjucks)

  • koa-session pour une protection de base par mot de passe (pas pour la production)

Servir des actifs statiques

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

Il s'agit probablement de la partie la moins compliquée à couvrir, le service de ressources statiques telles que CSS et Javascript côté client à partir du répertoire /public du dossier /public.

Routage et rendu de base

L'objectif est de faire en sorte que tout fonctionne sans aucun Javascript côté client. Les entrées des utilisateurs sont donc soumises via des formulaires HTML et, si nécessaire, une redirection vers la page appropriée après la soumission. Chaque page est son propre fichier HTML et est rendue avec koa-viewsqui fournit une fonction render() fonction.

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 fournit également un moyen d'accéder aux paramètres d'URL via ctx.paramsutilisé pour */verify/* et /result/**, qui est utilisé pour faire correspondre les numéros de téléphone au code de vérification généré. Cela aura plus de sens lorsque nous verrons comment fonctionne l'API Verify.

Verify API

L'utilisation de l'API Verify comporte deux étapes. La première consiste à déclencher l'envoi d'un SMS contenant le mot de passe à usage unique (OTP) au numéro de téléphone du destinataire cible.

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

La réponse HTTP de l'API Verify request ressemble à ceci :

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

Si le status est différent de 0la demande n'a pas abouti et les détails concernant les raisons de cet échec se trouvent dans la section error_text. Dans le cas contraire, le destinataire cible devrait recevoir un OTP par SMS. Il doit ensuite saisir cet OTP dans votre application, qui peut être vérifié par rapport à l'OTP original. request_id.

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

La réponse HTTP de l'API Verify ressemble à ceci :

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

Gestion de plusieurs joueurs

L'administrateur du point de contrôle doit garder une trace de tous les joueurs, il peut donc être judicieux d'utiliser une base de données pour stocker ces informations. Cette démo utilise lowdbqui est une petite base de données JSON locale basée sur Lodash, mais vous êtes libre d'utiliser n'importe quelle base de données.

Il existe un certain nombre de fonctions liées à la base de données pour ajouter des joueurs, récupérer des informations sur les joueurs et mettre à jour les informations sur les joueurs. En organisant les fonctions de cette manière, il devient plus facile de changer de base de données car la logique reste la même.

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

Vous pouvez utiliser un formulaire web pour collecter les informations nécessaires à la création d'un nouveau lecteur. Pour cette démo, seuls 2 champs sont nécessaires, le nom du joueur et son numéro de téléphone.

<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>

La soumission de ce formulaire enverra une requête POST à /addce qui signifie que vous devez créer une route pour gérer ces données entrantes, puis les stocker dans la base de données.

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

Nunjucks permet ensuite de restituer les informations de la base de données sur la page. Nunjucks fournit une fonction render() qui vous permet de passer des données à votre modèle Nunjucks. Modifiez la GET pour / afin de rendre les informations sur les joueurs de la base de données sur la page.

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

Vous pouvez ensuite insérer les valeurs des lecteurs dans le modèle et les rendre de la manière dont vous souhaitez que les données des lecteurs soient structurées.

Displaying player data on the frontendDisplaying player data on the frontend

Déclenchement de l'OTP

L'API Verify API Verify requiert le numéro de téléphone du joueur pour déclencher la demande OTP. Vous pouvez envoyer ces informations au backend par le biais d'un autre formulaire Web, cette fois-ci à l'adresse */verify/{{ player.phone }}*ce qui vous permet d'obtenir le numéro de téléphone par l'intermédiaire de ctx.params.phone et de le transmettre à la fonction de demande de l'API Verify.

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

Les request_idqui fait partie de la réponse de l'API, sera nécessaire pour la fonction check() suivante. Cette fonction sera utilisée pour vérifier le code request_id par rapport au code saisi par le joueur. Celui-ci est stocké dans la base de données via la fonction dbAddId() fonction.

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

Vérification de l'OTP

Les joueurs peuvent saisir le code de vérification à l'URL unique, */verify/{{ player.phone }}*. Vous devrez renvoyer le numéro de téléphone à la page afin que votre itinéraire ressemble à celui de la page d'accueil. GET ressemblerait à /qui comprend la fonction render() fonction.

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

Le formulaire web permettant aux joueurs de saisir leur OTP ressemblera à ceci :

<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

Lorsque le joueur soumet l'OTP, vous pouvez récupérer l'original dans la base de données. request_id original de la base de données et transmettre l'OTP et le PIN à l'API de vérification. request_id et le code PIN à la fonction check() de l'API de vérification.

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

Une vérification réussie renvoie un code d'état de 0que vous pouvez utiliser pour déterminer le message d'état à afficher à l'utilisateur sur la page de résultats.

<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

Autres actions possibles

Nexmo's Messages API de Nexmo peut également être utilisée pour ajouter des fonctionnalités supplémentaires à ce projet. Vous pourriez déclencher un SMS supplémentaire contenant le lien vers la page de vérification pour une meilleure expérience utilisateur, par exemple.

Ou peut-être votre jeu se déroule-t-il dans un endroit assez éloigné, pensez à Pulau Perhentian (photo ci-dessous), ou que, pour une raison ou une autre, vos joueurs ne disposent pas de données mobiles. Ainsi, au lieu de vérifier le code via une interface web, les joueurs peuvent répondre par SMS.

 Pulau Perhentian Pulau Perhentian

La version hébergée sur Glitch couvre les 2 scénarios ci-dessus, alors n'hésitez pas à la consulter et à la remixer.

remix this

Ce prototype est une démonstration de ce qui est possible avec l'API Verify, mais encore une fois, pour que cela devienne une application à part entière, il y a de nombreuses choses qui doivent être prises en compte. L'une des plus importantes est la gestion des erreurs. L'API Verify renvoie une valeur d'état de 0 pour les requêtes réussies, mais toute autre valeur indique une erreur.

Ces erreurs doivent être gérées et l'interface utilisateur sur le frontend doit refléter les erreurs potentielles empêchant une vérification réussie. Il peut également être judicieux de mettre en œuvre une sorte de validation frontale, ou même d'utiliser l'interface Nexmo Number Insight API de Nexmo de Nexmo pour s'assurer que seuls des numéros de téléphone valides sont transmis à l'API Verify.

Quelle est la prochaine étape ?

Si vous souhaitez en savoir plus sur ces API, voici quelques liens qui pourraient vous être utiles :

Partager:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing ChenAnciens de Vonage

Hui Jing est développeuse chez Nexmo. Elle a un amour immodéré pour le CSS et la typographie, et est généralement passionnée par tout ce qui touche au web.