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

NexmoのVerify APIとKoa.jsでチェックインアプリを作る

最終更新日 April 26, 2021

所要時間:5 分

例えば、次のようなゲームを運営しているとしよう。 アメージングレースのようなゲームを運営していて、プレイヤーがゲームをクリアする前に物理的に到達しなければならない複数のチェックポイントがあるとします。このアプリケーションは、プレイヤーがチェックポイントに到達したかどうかを追跡する方法です。

2FAについて

典型的な 2 要素認証フローでは、関係者は 1 人だけである。携帯電話番号を入力して認証コードのSMSを受信し、そのコードをユーザー・インターフェースに入力して本人認証を行う。簡単、簡単、レモンスクイージー。

これを2人だけのものにしたい場合は、もう少し面白くなる。チェックポイントの管理者は、全プレイヤーのリストとそれに対応する電話番号を持っている。プレーヤーがチェックポイントに到達すると、管理者はプレーヤーの携帯電話に認証コードを送信する。

プレーヤーは、チェックポイントにいることを確認するために、オンラインのウェブフォームから認証コードを入力するか、SMSで応答する。理論的には、管理者は検証コードにアクセスする方法がないため、チェックポイントにいないプレーヤーを検証することはできない。

しかし、これはMVPであり、今後のリリースで不正防止機能(およびその他の無数の機能)を再検討する予定です。たぶんね。

利用図書館

Nexmoの Verify APIは通常、二要素認証やパスワードレス認証に使われる。セキュアなOTP生成機能を自分で書くよりも、私はこれを認証コードに使うことにした。

Koa.jsは、アプリケーションの背後にあるフレームワークで、配信、ルーティング、APIリクエストとレスポンスの処理などを行います。コアとなるKoa.jsフレームワークは骨太なので、必要に応じて様々なミドルウェアを追加する必要があります。

Nunjucksはフロントエンドでデータをレンダリングするためのテンプレートエンジンであり、一方 lowdbはとてもシンプルなJSONデータベースで、このアプリケーションのようなプロトタイプには最適だ。データベース関連の機能はすべて、もっと "本格的な "データベースと簡単に交換することができる。

Glitch上の基本的なKoa.jsアプリケーション

すでに グリッチを使用している場合は すべて読み飛ばしてください.Glitchという素晴らしいプラットフォームをまだ知らない人たちのために、最初に着陸したときに、作りたいプロジェクトの種類を選ぶことができます。シンプルなウェブサイト(バックエンドなし)、Nodeアプリケーション、SQliteデータベース付きNodeアプリケーションの3つのプリセットがあります。このデモでは、2番目のオプションを選択します。

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

プロジェクトが持続するようにするには、Glitchアカウントにサインアップするのが良いだろう。Glitchはかなり頻繁に機能改善を行っているので、ずっと先のことを読んでいる場合は変わっているかもしれないが、執筆時点ではFacebook、GitHub、メール、サインインコードによるサインインをサポートしている。

Sign in to a Glitch accountSign in to a Glitch account

デフォルトでは、GlitchのNodeアプリケーションはExpressで実行される。このプロジェクトではKoa.jsを使用しているため、さらにいくつかのステップを踏む必要がある。

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

画面左下の「ツール」をクリックすると、ログ、コンソール、コンテナ統計などのオプションが表示される。

Tools options on GlitchTools options on Glitch

アプリケーションを開発するときにログを開いておくと便利です。 console.log()がここに表示されるからだ。

Viewing logs on GlitchViewing logs on Glitch

プロジェクトで使いたいnpmモジュールをカスタマイズするには、ローカルマシンやリモートサーバーと同じようにコマンドラインにアクセスする。注意点としては npmの代わりに pnpmをパッケージ・マネージャーとして使用することです。

Accessing the Glitch consoleAccessing the Glitch console

以下を実行してexpressを削除する:

pnpm uninstall express

次に、以下を実行してKoa.jsをインストールする:

pnpm install koa --save

プロジェクトで使用されているnpmモジュールを検証するには、環境を更新する必要があります:

refresh

そうすると、Toolsの横に "Error "と表示されるはずです。これは問題ない。 server.jsファイルにはExpressフレームワークが必要なのですが、これはもう存在しないからです。

次にすることは、基本的なサーバーコードをKoa.jsを使うように書き換えることです。以下のコードを新しく作成したファイルに貼り付けてください。

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

すべてがうまくいっていれば、上部のナビバーにある「Show」ボタンをクリックすると、新しいウィンドウに「Hello Dinosaur 🦖」というテキストとともにアプリケーションが表示されるはずです。

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

アプリケーションの構造

本来であれば、管理者のための適切なユーザー管理があるはずだ。Glitchのライブ・デモにアクセスすれば、パスワードによる保護があることがわかるだろうが、このシンプルなプロトタイプとしては非常に素朴な実装だ。このプロトタイプは、単にインターフェイスがどのように機能するかのアイデアを提示しているに過ぎない。

つまり、アプリケーションは3つのページ、ログインページ、管理者ページ、検証コード入力ページを持つことになります。

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

先に述べたように、いくつかの追加ミドルウェアをインストールする必要がある。このプロジェクトでは以下を使用する:

  • koa-static静的アセット提供用

  • koa-bodyparserPOSTリクエストで送られたデータを処理する

  • koa-routerルーティング用

  • koa-viewsnunjucksテンプレートのレンダリング用(nunjucksがインストールされている必要があります。)

  • koa-session基本的なパスワード保護用(本番用ではない)

静的アセットの提供

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

CSSやクライアントサイドのJavascriptのような静的アセットを /パブリックフォルダから提供されます。

基本的なルーティングとレンダリング

計画では、クライアントサイドのJavascriptを使わずにすべてを動作させるつもりです。そのため、ユーザーの入力はHTMLフォームで送信され、必要に応じて、送信後に適切なページにリダイレクトされます。各ページはそれ自身のHTMLファイルであり、レンダリングには koa-viewsでレンダリングされます。 render()関数でレンダリングされます。

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を介してURLパラメータにアクセスする方法も提供する。 ctx.paramsを介してURLパラメータにアクセスする方法も提供している。 および/これは電話番号と生成された検証コードを照合するために使用される。これは、Verify APIがどのように動作するかを説明すれば、より理解できるだろう。

Verify API

Verify APIを使用する際には、2つのステップがあります。1つ目は、ワンタイムパスワード(OTP)を含むSMSをターゲット受信者の電話番号に送信することです。

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

VerifyリクエストAPIからのHTTPレスポンスは以下のようになる:

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

もし status0以外の場合、リクエストは失敗し、その理由の詳細は error_text.それ以外の場合、対象となる受信者はSMSでOTPを受け取る必要があります。受信者はこのOTPをアプリケーションに入力する必要があります。 request_id.

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

Verify check APIからのHTTPレスポンスは以下のようになる:

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

複数選手への対応

チェックポイントの管理者はすべてのプレーヤーを追跡する必要があるので、その情報を保存するためにデータベースを使用するのは良いアイデアかもしれません。このデモでは lowdbを使っていますが、どんなデータベースを使ってもかまいません。

選手の追加、選手情報の取得、選手情報の更新など、データベース関連の関数は数多くあります。このように関数を整理することで、ロジックが変わらないため、データベースの入れ替えが容易になります。

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

ウェブフォームを使用して、新しいプレーヤーを作成するために必要な情報を収集できます。このデモでは、プレーヤー名と電話番号の2つのフィールドのみが必要です。

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

このフォームを送信すると、POST リクエストが以下に送信されます。 /にPOSTリクエストを送信します。にPOSTリクエストを送るので、この入力データを処理するルートを作り、それをデータベースに保存する必要があります。

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

そして、Nunjucksを使ってデータベースからの情報をページにレンダリングすることができます。Nunjucksは render()関数が用意されており、Nunjucksテンプレートにデータを渡すことができます。この関数を使用して GETルートを /のルートを変更して、データベースからページに選手情報をレンダリングできるようにします。

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

その後、プレーヤーの値をテンプレートに差し込み、プレーヤーのデータをどのような構造にしてもレンダリングできます。

Displaying player data on the frontendDisplaying player data on the frontend

OTPのトリガー

について ベリファイAPIは OTP リクエストのトリガーとしてプレーヤーの電話番号を要求します。この情報をバックエンドに送信するには、別のウェブフォームを使用します。 */verify/{{ player.phone }}*に投稿し、電話番号を取得します。 ctx.params.phoneで電話番号を取得し、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()
}

APIレスポンスの一部である request_idAPIレスポンスの一部である check()関数で必要となる。この関数は、プレーヤーが入力したコードに対して request_idを検証するために使用されます。これは dbAddId()関数を介してデータベースに保存されます。

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

OTPの確認

プレイヤーは固有のURLから認証コードを入力することができます、 */verify/{{ player.phone }}*.電話番号をページに戻す必要があります。 GETルートは /を含む render()関数が含まれます。

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

プレーヤーがOTPを入力するためのウェブフォームは次のようになります:

<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

プレーヤーがOTPを送信すると、データベースから元のOTPを取得することができます。 request_idをデータベースから取得し request_idと PIN の両方を 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()
}

検証に成功すると、ステータスコード 0を返します。 このコードを使って、結果ページでユーザに表示するステータスメッセージを決定できます。

<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

その他できること

ネクスモ メッセージAPIを使用して、このプロジェクトに機能を追加することもできます。例えば、より良いユーザーエクスペリエンスのために、認証ページへのリンクを含む追加のSMSをトリガーすることができます。

あるいは、ゲームの舞台がかなり離れた場所かもしれない。 ペルヘンティアン島(下の写真)のようなかなり離れた場所で行われたり、何らかの理由でプレーヤーがモバイルデータを持っていなかったりするかもしれません。そこで、ウェブインターフェイスでコードをチェックする代わりに、プレイヤーはSMSで応答することができます。

 Pulau Perhentian Pulau Perhentian

でホストされているバージョン グリッチ上記の2つのシナリオをカバーしているので、自由にチェックしてリミックスしてほしい。

remix this

このプロトタイプはVerify APIで何が可能かを示すものだが、これが本格的なアプリケーションになるためには、やはり多くのことに注意しなければならない。最も重要なことの一つはエラー処理である。Verify APIは、クエリーが成功した場合はステータス値として 0を返しますが、それ以外の値はエラーを示します。

これらのエラーを処理し、フロントエンドのユーザーインターフェイスに、検証の成功を妨げる潜在的なエラーを反映させる必要があります。また、フロントエンドで何らかの検証を実装したり、Nexmoの Number Insight APIを利用し、有効な電話番号のみがVerify APIに渡されるようにするのもよいだろう。

次はどこだ?

これらのAPIをもっと使いたいとお考えなら、以下のリンクが参考になるでしょう:

シェア:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing Chenヴォネージの卒業生

ホイ・ジンはNexmoのデベロッパー・アドボケイト。CSSとタイポグラフィをこよなく愛する。