IBM i 上でNode.jsのWebアプリを動かす その4 (QR読み取りとアプリ詳細)

# はじめに

「IBM i 上でNode.jsのWebアプリを動かす」3つ目の記事です。  
- 1つ目の記事はこちら  
  IBM i 上での Node.js の動かし方を簡単に紹介しています。

 https://qiita.com/tom_m_m/items/5cd480eb1aad35a6a6dc

- 2つ目の記事はこちら  
環境情報や作成したWebアプリの概要はこちらをご覧ください。

https://qiita.com/tom_m_m/items/1d0139cc8b507be4b591

- 3つ目の記事はこちら
Express, EJSについてまとめています。  
https://qiita.com/tom_m_m/items/8bc53ae27a396946c614

作成したコードはこちらにおいています。  

https://github.com/tom-m-m/ibmi-nodejs-qr-detection

この記事の中では、こちらのアプリを「QRコードアプリ」と記載しています。  
Node.jsでのWebアプリ制作に関しては手探りで行っていますので、荒いところがあるかもしれません。

# JavaScriptでのQRコード読み取り

QRコードの読み取りには、こちらで公開されている jQR を利用しています。

https://github.com/cozmo/jsQR

また、こちらの記事を参考にさせていただいております。  

https://qiita.com/youtoy/items/d94a5bf835d3f4007c81

jsQRの使い方については、上記2つの記事を参考にしていただければと思います。  
ここでは、QRコードアプリを作成した際に詰まった部分について記述します。  

当初は、QRコード読み取り用のJavaScriptを別のファイルとして用意していました。  
ただ、express, ejs の組み合わせで、うまくデータの受け渡しができず、ejsファイル内に JavaScript をベタがきしています。  
良くないとは思いつつ、自分の技術力不足のためこの形になっています。  

## httpsでのアクセス

カメラをブラウザ上に表示する際、環境によっては `https` でのアクセスが必須になるようです。  
今回はこれに該当したため、自己署名証明書を作って簡易的にhttpsアクセスが出来るよう設定しています。  
httpsでアクセスができない場合、カメラが起動しない状態となります。 

httpsアクセスに関しては、こちらを参考にしています。  

https://kaworu.jpn.org/javascript/node.js%E3%81%AB%E3%82%88%E3%82%8BHTTPS%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9

まずは、自己署名証明書を作成します。  
QRコードアプリでは、key ディレクトリを作成して、そこに証明書のファイルを配置しています。  
下記のように実行すると、証明書が作成できます。  
```
key # openssl genrsa -out server_key.pem 2048
key # openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Tokyo/L=Musashino-shi/O=Foo/OU=Bar/CN=foo.bar.com"
key # openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Certificate request self-signature ok
subject=C = JP, ST = Tokyo, L = Musashino-shi, O = Foo, OU = Bar, CN = foo.bar.com
```

これを前回の記事で作成したExpress + EJSの簡易的なWebアプリを変更して https でアクセス出来るようにします。  
前回作成したコードは、こちらでした。  
```
express-test # cat index.js 
const express = require('express');

const app = express();

app.set("view engine", "ejs");

const hostname = '<サーバーのIPアドレス>';
const port = 9443;

app.get('/', (req, res) => {
  res.render('index', {data: 'jsから渡したデータ'});
});

app.listen(port, () => console.log(`Listening on port ${port}!`));
```

このコードを以下のように書き換えています。  
1. パッケージ `fs` の追加
2. 証明書の読み込みとhttpsサーバーの作成
3. appではなくserverでの起動に変更

```
express-test # cat index.js 
const express = require('express');
var fs = require('fs'); //追加 1

const app = express();

app.set("view engine", "ejs");

//追加 2
const server = require('https').createServer(
  {
    key: fs.readFileSync('./key/server_key.pem'),
    cert: fs.readFileSync('./key/server_crt.pem'),
  },
  app
);

const hostname = '<サーバーのIPアドレス>';
const port = 9443;

app.get('/', (req, res) => {
  res.render('index', {data: 'jsから渡したデータ'});
});

server.listen(port, () => console.log(`Listening on port ${port}!`)); //変更 3
```

この変更を行うことで、httpsでのアクセスが可能になります。  

ブラウザでアクセスすると、最初は警告画面が出るので、詳細設定を開いた上でページにアクセスします。    

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242652/05c94ddc-5bb6-3f07-b27f-ae088a74641b.png)

アクセスすると、httpsで表示されていることがわかります。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242652/b04a9940-c5e6-fd01-9ef1-71f98ec43f08.png)

IBM i 上でjsQRを動かすときには、このhttpsの設定が必要でした。  

# QRコードアプリ詳細

ここまで、QRコードアプリを構成しているパーツについて書いてきました。  
それでは、実際に作成したアプリの動きを見ていきます。  
画面については、あまりカスタマイズしてないためシンプルな見た目となってます。  

アクセスすると、このような画面が表示されます。  
index.ejs が表示されている状態となり、jqQRがブラウザ側で動いている状態です。  
黒色で表示されているのが、カメラ画面になります。  


<details><summary>index.ejs</summary><div>

```html:index.ejs
<!DOCTYPE html>
<html lang="jp">
<head>
    <%- include('./partials/head'); %>
    <link rel="stylesheet" href="css/index.css">
</head>
<body class="container">

<header>
    <%- include('./partials/header'); %>
</header>

<main>
    <div id="loadingMessage"> カメラが有効になっていません</div>
    <canvas id="canvas" hidden></canvas>
    <div id="outputs" hidden>
      <div id="outputMessage">QRコード検出なし</div>
      <div hidden><b>登録番号:</b> <span id="outputData"></span></div>
      <div hidden><b></b> <span id="result"></span></div>
    </div>
  
    <div id="outputs" hidden>
        <button type="button" class="btn btn-primary" onclick="yes()">Yes</button>
        <button type="button" class="btn btn-secondary" onclick="no()">No</button>
    </div>

</main>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>

<script type="text/javascript">
var video = document.createElement("video");
var canvasElement = document.getElementById("canvas");
var canvas = canvasElement.getContext("2d");
var loadingMessage = document.getElementById("loadingMessage");
var outputContainer = document.getElementById("outputs");
var outputMessage = document.getElementById("outputMessage");
var outputData = document.getElementById("outputData");
var result = document.getElementById("result");
var requestID;

function drawLine(begin, end, color) {
  canvas.beginPath();
  canvas.moveTo(begin.x, begin.y);
  canvas.lineTo(end.x, end.y);
  canvas.lineWidth = 4;
  canvas.strokeStyle = color;
  canvas.stroke();
}

// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then(function(stream) {
  video.srcObject = stream;
  video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
  video.play();
  requestAnimationFrame(tick);
});


var qr_flag = JSON.parse("<%= flag %>".replace(/&#34;/g,'"'));
var update_flag = JSON.parse("<%= update_flag %>".replace(/&#34;/g,'"'));

if (update_flag == 1){
  outputMessage.innerText = '入室処理が完了しました。';
} else if (update_flag == 2){
  outputMessage.innerText = '退室処理が完了しました。';
} else if (update_flag == 3) {
  outputMessage.innerText = 'すでに処理が完了しているため、変更されませんでした。';
}

function tick() {
  loadingMessage.innerText = "Loading video..."
  if (video.readyState === video.HAVE_ENOUGH_DATA) {
    loadingMessage.hidden = true;
    canvasElement.hidden = false;
    outputContainer.hidden = false;

    canvasElement.height = video.videoHeight;
    canvasElement.width = video.videoWidth;
    canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
    var imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
    var code = jsQR(imageData.data, imageData.width, imageData.height, {
      inversionAttempts: "dontInvert",
    });

      if (code) {
        drawLine(code.location.topLeftCorner, code.location.topRightCorner, "#FF3B58");
        drawLine(code.location.topRightCorner, code.location.bottomRightCorner, "#FF3B58");
        drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, "#FF3B58");
        drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, "#FF3B58");
        outputMessage.hidden = true;
        outputData.parentElement.hidden = false;
        outputData.innerText = code.data;

        if (code.data) {
          post("/qrid", {id:code.data});
          sleep(300);
        } else {
          result.parentElement.hidden = false;
        }

      } else {
        outputMessage.hidden = false;
        outputData.parentElement.hidden = true;
        result.parentElement.hidden = true;
      }
    }
    requstID = requestAnimationFrame(tick);
}

function post(path, params, method='post') {
  const form = document.createElement('form');
  form.method = method;
  form.action = path;

  for (const key in params) {
    if (params.hasOwnProperty(key)) {
      const hiddenField = document.createElement('input');
      hiddenField.type = 'hidden';
      hiddenField.name = key;
      hiddenField.value = params[key];

      form.appendChild(hiddenField);
    }
  }

  document.body.appendChild(form);
  form.submit();
}

function sleep(waitMsec) {
  var startMsec = new Date();

  while (new Date() - startMsec < waitMsec);
}
</script>


</body>
</html>
```
</div></details>  
  

![スクリーンショット 2023-12-14 19.24.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242652/73c95eb9-65cd-1397-f240-27271d598fd2.png)

QRコードを写すと、一瞬QRコードの部分が赤枠で囲われ、次の画面に移ります。  
読み取りが完了すると、読み取ったIDをPOSTリクエストでサーバー側に送信します。  

![スクリーンショット 2023-12-14 19.24.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242652/284bf033-e3b5-70a3-0450-a054152cfa9d.png)

QRコードを読み取り、IBM i のDBに一致するIDが存在する場合は、処理を行うかどうかの選択肢が表示されます。  
POSTリクエストを受け付けると、サーバー側では、idb-connect を介してデータが存在するかどうか確認するためのSQLが実行されています。  

```
app.post('/qrid', (req, res) => {
  if (req.body.id != null){
    const sql = `SELECT * FROM ACCESS.ACCESSTABLE WHERE QRID = ${req.body.id} LIMIT 1`;
    const connection = new dbconn();
    connection.conn('*LOCAL');
  
    const statement = new dbstmt(connection);
    const result = statement.execSync(sql);

    let qrid = JSON.stringify(result[0].QRID);
    let name = JSON.stringify(result[0].NAME);
    let status = JSON.stringify(result[0].STATUS);

    statement.close();
    connection.disconn();
    connection.close();

    res.render('submitqr.ejs', {qrid : qrid, name : name, status : status, flag : 1});
  }
});
```

![スクリーンショット 2023-12-14 19.24.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242652/578ff85b-537c-0a26-d4dc-66a6b4d1e34f.png)

`はい` を選択すると、POSTリクエストが送信され、サーバー側ではDBのデータ更新が行われます。  
今回のアプリだと、予約状態から入室状態に、入室状態から退出状態に STATUS を変更します。  

<details><summary>入退出のDB更新部分</summary><div>

```
app.post("/submit",(req,res)=>{
  let update = 0;

  const connection = new dbconn();
  connection.conn('*LOCAL');
  const statement = new dbstmt(connection);

  const sql = `SELECT * FROM ACCESS.ACCESSTABLE WHERE QRID = ${req.body.id} LIMIT 1`;
  const result = statement.execSync(sql);

  let qrid = JSON.stringify(result[0].QRID).slice( 1, -1 );
  let status = JSON.stringify(result[0].STATUS).slice( 1, -1 );

  const statement_update = new dbstmt(connection); 

  if (status == 'schedule'){
    const sql_up = `UPDATE ACCESS.ACCESSTABLE SET STATUS = 'stay' WHERE QRID = ${qrid}`;
    const result = statement_update.execSync(sql_up);
    update = 1;
  } else if (status == 'stay'){
    const sql_up = `UPDATE ACCESS.ACCESSTABLE SET STATUS = 'leave' WHERE QRID = + ${qrid}`;
    const result = statement_update.execSync(sql_up);
    update = 2;
  } else {
    update = 3;
  } 

  statement.close();
  statement_update.close();
  connection.disconn(); 
  connection.close();

  res.render('index.ejs', {update_flag : update, flag : 0});
})

```

</div></details>



![スクリーンショット 2023-12-14 19.24.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242652/b7320f43-95c5-e871-f33e-6ac301b72f35.png)

一連の流れは以上になります。  

アプリ全体のコードはこちらにおいてありますので、コピーすれば試すことができます。  
パッケージのインストールとと証明書の作成、DBの作成を行うことで動かすことができます。  

https://github.com/tom-m-m/ibmi-nodejs-qr-detection

動かすまでの手順は以下になります。  
- Githubからのソースダウンロード  

```
work # git clone https://github.com/tom-m-m/ibmi-nodejs-qr-detection.git
Cloning into 'ibmi-nodejs-qr-detection'...
remote: Enumerating objects: 40, done.
remote: Counting objects: 100% (40/40), done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 40 (delta 15), reused 35 (delta 10), pack-reused 0
Receiving objects: 100% (40/40), 3.93 MiB | 3.15 MiB/s, done.
Resolving deltas: 100% (15/15), done.
```

- パッケージのインストール  
  Githubにはモジュールをアップロードしていないため、インストールが必要となっています。  
  package.json があると、`npm install` で必要なパッケージがインストールできます。  

```
work # cd ibmi-nodejs-qr-detection/
ibmi-nodejs-qr-detection # ls
app.js  core  odbc.ini  package-lock.json  package.json  views
ibmi-nodejs-qr-detection # npm install

added 216 packages, and audited 217 packages in 9s

24 packages are looking for funding
  run `npm fund` for details

8 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
```

- 証明書の作成  
  証明書は、この記事の上部で記載している内容と同様になります。  

```
ibmi-nodejs-qr-detection # mkdir key
ibmi-nodejs-qr-detection # cd key/
ibmi-nodejs-qr-detection/key # openssl genrsa -out server_key.pem 2048
ibmi-nodejs-qr-detection/key # openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Tokyo/L=Musashino-shi/O=Foo/OU=Bar/CN=foo.bar.com"
ibmi-nodejs-qr-detection/key # openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Certificate request self-signature ok
subject=C = JP, ST = Tokyo, L = Musashino-shi, O = Foo, OU = Bar, CN = foo.bar.com
ibmi-nodejs-qr-detection/key # cd ..
ibmi-nodejs-qr-detection # node app.js 
Listening on port 9443!
```
あとは、DBの作成を行うと完了です。  
IDを含むQRコードは、Webのサービスを使うと簡単に作成できます。  
Node.js にもQRコード作成用のパッケージもあります。  

4編に渡る記事になりましたが、このシリーズは終了となります。  
ありがとうございました。