ExpressとPostgresqlを使ってHerokuでログインページを作る

# 今回の内容
前回、[HerokuでExpressアプリを作る---基本設定](https://qiita.com/forl_head_officer/items/4af1661bb3b586624a6e)、の続き。
今回はPostgreSQLを設定しユーザーを設定した簡単なテーブルを使ったログインページを作る。

## 環境整備
データーベースにはクレジットカード登録なしで使えるPostgreSQLを選んだ。
Postgresqlをnode.jsで使うためのライブラリとしてPromise形式になっている[pg-promise](http://yamada.daiji.ro/blog/?p=756)を使う。
リクエストにログイン情報を載せるためにbodyを使うのでbody-parser、cookieによるログインも使うのでcookie-parserを使う。
これらは以下のコマンドでできる

```shell-session
$ yarn add body-parser cookie-parser pg-promise
$ heroku addons:create heroku-postgresql:hobby-dev
```

## PostgreSQLの整備
ログインユーザー用のデータとしてloginUserを作っておく。
herokuのpostgreSQLには`heroku pg:psql`でログインできるので以下のようにしてデータを作っておく。

```shell-session
$ heroku pg:psql
> create table loginUser ( text name, text password);
> insert into member( name, password ) values ( 'admin', 'hogehoge' );
> select * from loginUser;
//*** ユーザー情報の表示 ***//
> \q
```

## index.js
インストールしたパッケージとログインページの作成をする。ログインページはprivate/ディレクトリに置いておく(公開ページはpublic/)
ログインは`POST`で受付、パスワードとユーザー名が一致したらステータス200で一致しない場合、401で返す[^status]。
user_only.htmlにアクセスがあった場合、クッキーを見てクッキーのユーザー名とパスワードが一致すれば`private/user_only.html`を一致しなければログインページを返すようにしてある。注意点として`sendStatus`をifの中書いて`send`を外に書くとindex.js自体が死んだ。

[^status]: [【図解】RESTful WebサービスにおけるHTTPステータスコード](https://www.agilegroup.co.jp/technote/rest-status-code.html)

```js:index.js
const path=require('path');

const express=require('express');
const app=express();
const cookieParser=require('cookie-parser');
const bodyParser=require('body-parser');

// noWarning: true の設定がないと落ちる。databaseURLはherokuでは環境変数に入っている
const pgPromise=require('pg-promise')({ noWarnings: true });
const databaseURL=process.env.DATABASE_URL;

const database=pgPromise(databaseURL);

app.set('port', process.env.PORT || 5050);
app.use(express.static(path.join(__dirname, 'public')));
app.use(log.access.add);
app.use(cookieParser());
// bodyParserの設定 (Json形式用)
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.post('/login', async (req, res)=>{
    const user= await database.any('select * from loginUser').filter(a=> a.name===req.body.name);

    if( user.length!==1 || user[0].password!==req.body.password ) res.sendStatus(401);
    else res.send('OK');
});

app.get('/status.html', async (req, res)=>{
    const user= await database.any('select * from loginUser').filter(a=> a.name===req.body.name);
                                                       
    if( user.length!==1 || user[0].password!==req.cookies.password ){
        res.sendFile(path.join(__dirname, '/private/login.html')); 
    }
    else{ res.sendFile(path.join(__dirname, '/private/user_only.html')); }
});
```

## クライアント側
```html:private/login.js
<!doctype html>
<html>
  <head>
     <script src="js/password.js"></script>
  </head>
  <body>
     ユーザー名<input type="text" id="name"></input>
     パスワード<input type="password" id="password"></input>
     <button type="button" id="send">送信</button>
  </body>
</html>
```
クライアント側では送信ボタンが押されるとinputの情報をとってきてResponseを見て成功なら確認してクッキーに書き込んでページ再読込でサーバー側でhtmlファイルを切り替えている

```js/private/js/login.js
window.addEventListener('DOMContentLoaded', ()=>{
    document.getElementById('send').addEventListener('click', ()=>{
        const name=document.getElementById('name').value;
        const password=document.getElementById('password').value;

        const method='POST';
        const headers={ 'Accept': 'application/json', 'Content-Type': 'application/json' };
        const body=JSON.stringify({ 'name': name, 'password': password });
        fetch('login', { method, headers, body }).then(res=>{
            if( res.ok ){
                if( getCookie('name')==null && getCookie('password')==null &&
                    confirm('パスワードを保存しますか？\n(クッキーを使用します)') ){
                    console.log('パスワードを保存しました');
                    document.cookie=`name=${name}`
                    document.cookie=`password=${password};
                }
                location.reload();
            }
            else alert('ログインに失敗しました');
        });
    });
});
```

## 最後に
今回は簡単なデータベースを使ってサーバー側でhtmlファイルを切り替えた。色々やるためにはSQLの知識などは全く足りない。
クライアント側のUIの整備などもほとんどしていないので工夫が必要であるが簡単な使い方を紹介しました。