node.jsでAPIサーバみたいなものを作ってみる




# 動作確認用にデータベースを準備する

dockerをつかう。

データベース用にフォルダを作って、そのなかにdocker-compose.ymlをおく。

```database/docker-compose.yml
services:
    db:
        image: postgres:13
        container_name: postgresql
        ports:
            - 5432:5432
        environment:
            POSTGRES_USER: postgres
            POSTGRES_PASSWORD: postgres
            POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --no-locale"
        volumes:
            - ./pgdata:/var/lib/postgresql/data
            - ./pginit:/docker-entrypoint-initdb.d
```

同じフォルダにpginitというフォルダを掘って、以下のようなファイルを置いておく。

```database/pginit/0.sql
CREATE TABLE Users (
    user_id INTEGER,
    username TEXT,
    password TEXT,
    realname TEXT
);

INSERT INTO Users VALUES(
    1,
    'user01',
    '0497fe4d674fe37194a6fcb08913e596ef6a307f',
    '私です');
INSERT INTO Users VALUES(
    2,
    'user02',
    'a7659675668c2b34f0a456dbaa508200340dc36c',
    'あなたです');
```

で、ターミナルをいっこ準備して起動しておく。

```
docker-compose up
```

# APIサーバらしきものを作る。


/loginにusernameとpasswordをPOSTするとJWTが帰ってきて、そのJWTをAuthorizationヘッダに設定したGETリクエストを/himitsuに投げると、そのユーザの本名が返ってくるものを作ってみる。

APIサーバ用のフォルダを作ってその中で以下の作業を行う。

まず使うライブラリ類をインストールしておく。

```
npm init -y
npm install express cors crypto jsonwebtoken pg
```

ばーっとコードを書く。普通はファイルを分割したりすると思うけれどこれは全容を理解することを目的に試作しているので、１ファイルにしておきました。

```app.js
// expressのアプリケーションを生成
const express = require('express')
const app = express()

// URLエンコードすかいます
app.use(express.urlencoded({ extended: true }))
// JSONつかいます
app.use(express.json())
// 全APIのCORSを許可
const cors = require('cors')
app.use(cors())　

// パスワードのハッシュ化用
const crypto = require('crypto')

// JWT用
const jwt = require('jsonwebtoken')
const secret_key = 'kokonianatanosecretkeywokaku';

// PostgreSQLへのコネクションプールの準備
const Pool = require('pg').Pool
const pool = new Pool({
    host: 'localhost',
    port: 5432,
    database: 'postgres',
    user: 'postgres',
    password: 'postgres'
})

// /loginにPOSTされたときの処理。
app.post('/login', async(req, res) => {
    // リクエストからユーザ名とパスワードを取り出す。パスワードはハッシュ化
    let username = req.body.username
    let shasum = crypto.createHash('sha1')
    shasum.update(req.body.password)
    let password = shasum.digest('hex')
    console.log(username, password);
    try {
        let result = await pool.query(
            'SELECT user_id FROM users WHERE username = $1 AND password = $2',
            [username, password])
        if(result.count == 0) {
            // 一致するユーザがいなかったので404を返す
            res.status(404).json({status: 'user not found'});
        } else {
            // 一致するユーザがいたのでトークンを生成して返却
            let signedtoken = jwt.sign({
                sub: result.rows[0]['user_id']
            }, secret_key, {expiresIn: '1h'})
            res.json({token: signedtoken})
        }
    } catch(e) {
        // なんかしらないけどエラーが出たので500を返す
        console.log(e);
        res.status(500).json({status: 'internal error'});
    }
});

// JWTを検証して埋め込んだデータを取り出す。
async function verify(req, res, next) {
    let token = '';
    if (req.headers.authorization &&
        req.headers.authorization.split(' ')[0] === 'Bearer') {
            token = req.headers.authorization.split(' ')[1];
            jwt.verify(token, secret_key, (err, payload) => {
                if(err) {
                    res.status(401).json({message: 'error'}); // 認証失敗
                } else {
                    req.user_id = payload.sub;
                    next();
                }
            });
    } else {
        res.status(401).json({message: 'error'});
    }
}

// /himitsuというURLにアクセスされたらトークンを検証して適切ならそのユーザの名前を返す。
app.get('/himitsu', verify, async(req, res) => {
    try {
        let result = await pool.query(
            'SELECT realname FROM users WHERE user_id = $1',
            [req.user_id])
        res.json({realname: `hello ${result.rows[0]['realname']}`});
    } catch(e) {
        res.status(400).json({status: 'bad request'});
    }
})

let port = 8080
app.listen(port, () => {
    console.log('ok', port);
})

```

起動する。

```
node ./app.js
```

# 適当な画面を作る。

reactのプロジェクトを作る。

```
npx create-react-app frontend
```

frontendというフォルダの中にいろいろできているので、そのなかのsrc/App.jsというファイルを以下のような感じに書き換える。

```src/App.js

import React, { useEffect, useState } from "react";

function App() {

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [token, setToken] = useState("");
  const [realname, setRealname] = useState("");

  useEffect(()=>{
    let url = 'http://localhost:8080/himitsu';
    fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + token,
      }
    }).then(res => {
      if(res.status !== 200) {
        throw Error("failed to get himitsu info");
      }
      return res.json();
    }).then(data => {
      console.log(data);
      setRealname(data.realname);
    }).catch(e => {
      console.log("errorrr");
    });
  },[token]);

  function submit(e) {
    console.log('submit');
    e.preventDefault();
    let url = 'http://localhost:8080/login';
    fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        username: username,
        password: password,
      })
    }).then(res => {
      console.log(res);
      if(res.status !== 200) {
        throw Error("failed to login")
        // TODO: ログイン失敗した旨を表示する。
      }
      return res.json();
    }).then(data => {
      setToken(data.token);
      console.log(token);
    }).catch(e => {
      console.log(e);
    })
  }

  return (
    <>
      <form onSubmit={submit}>
        <label htmlFor="username">username:</label>
        <input type="text" id="username" onChange={(e)=>setUsername(e.target.value)} /> <br/>
        <label htmlFor="password">password:</label>
        <input type="password" id="password"  onChange={(e)=>setPassword(e.target.value)} /> <br/>
       <button type="submit">ログイン</button>
      </form>
      <p>【{realname}】</p>
    </>
  );
}

export default App;

```

そして起動。

```
npm start
```

勝手にブラウザが開いてくるので、ユーザ名user01、パスワードuser01と入力してログインボタンを押すと、APIサーバから帰ってきた名前が表示される。ユーザ名user02、パスワードuser02でもいける。
ちなみに0.sqlというファイルでユーザアカウント情報を登録しているけれども、ぐちゃぐちゃした文字列はこのパスワードのハッシュをSHA1で求めたものです。