Netflix Falcorについて（５）

[Netflix Falcorについて（１）](http://qiita.com/sand/items/704dc924235b131b437c "Netflix Falcorについて（１）")
[Netflix Falcorについて（２）](http://qiita.com/sand/items/7c062a6e25352ad6949a "Netflix Falcorについて（２）")
[Netflix Falcorについて（３）](http://qiita.com/sand/items/02838dac3308a35cb51d "Netflix Falcorについて（３）")
[Netflix Falcorについて（４）](http://qiita.com/sand/items/9b581b82b6fe40cd1989 "Netflix Falcorについて（４）")
[Netflix Falcorについて（５）](http://qiita.com/sand/items/a2f764462b9c19092baf "Netflix Falcorについて（５）")


　今回は、前回のプログラムに認証の枠組みを加えます。login認証（ユーザ名・パスワード）を行い、JWT tokenを生成し、addの実行ロジックにtoken認証を加えます。但し注目点はあくまでもFalcorですので、パスワード認証やJWTの扱いはダミーとします。


　最初にクライアント側プログラムを見ましょう。

```index.html
<html>
  <head>
    <!-- Do _not_  rely on this URL in production. Use only during development.  -->
    <script src="//netflix.github.io/falcor/build/falcor.browser.js"></script>
    <script>

      var model = new falcor.Model({source: new falcor.HttpDataSource('/model.json') });

      async function fetch() {
//---------------login
        var credentials={ 'username': 'admin', 'password': 'dummy_pass' };
        var loginResult = await model.call ( ['login'], [credentials]).then((result) => {
                return(result);
            });
        var tokenRes = await model.getValue('login.token'); // 結果はVirtual JSONの値からget
        localStorage.setItem("token", tokenRes); //  HTML5のlocalStorageにtokenを保存。クッキーは使わない

//---------------HttpDataSourceにおいてtokenをglobal HTTP requestsに追加する
        var headers;
        if(localStorage.token) {
            headers = {
              headers: {
                'token': localStorage.token
              }
            };
        }

        // HttpDataSourceでは第２引数に、global HTTP requestsへの追加オプションを設定できます
        // Expressサーバの req.headers.token にtokenが渡されます
        model = new falcor.Model({source: new falcor.HttpDataSource('/model.json', headers) });
//---------------add articles
        // ①add前のlengthを取得
        await model.
          get("articles.length").
          then(function(response) {
            var res = JSON.stringify(response,null,2);
            console.log(res);
            document.write("<div>addの前"+res+"</div>");
          });


        // ②新articleのadd、新_id(newArticleID)をgetする
        var newArticle = {
            title: "テスト１",
            content: "これはテスト１のコンテンツです"
        };
        var newArticleID = await model.
            call( 'articles.add',[newArticle] ).then((result) => {
              var res2 = JSON.stringify(result,null,2);
              console.log(res2);
              document.write("<div>"+res2+"</div>");

              return model.getValue(
                  ['articles', 'newArticleID']
                ).then((articleID) => {
                  return articleID;
                });
            });
          document.write("<div>"+newArticleID+"</div>");


        // ③addした_idをもとに新articleの実体をget
        await model.
          get(['articlesById',[newArticleID],['_id','title', 'content']]).
          then(function(articlesResponse) {
            var res2 = JSON.stringify(articlesResponse,null,2);
            console.log(res2);
            document.write("<div>"+res2+"</div>");
          });


        // ④add後のlengthを取得
        await model.
          get("articles.length").
          then(function(response) {
            var res = JSON.stringify(response,null,2);
            console.log(res);
            document.write("<div>addの後"+res+"</div><br/>");
          });


      }

      fetch();

    </script>
  </head>
  <body>
  </body>
</html>
```


　まず最初にlogin pathを追加し、call()でサーバ側に認証依頼をリクエストします。サーバはJSON Graphの'login.token'パスにtoken値を返しますので、getValue()で値を取得します。取得したtoken値はHTML5のlocalStorageに保存しておきます。

　シナリオ的には、次のリクエストからはtokenを付けて送信し、add/delete/updateなどのアクセス権限が必要な時はtokenを見て判断することとします。クライアント側ではHttpDataSource()の第２引数にheaderオプションとしてtokenを指定してmodelを作成します。それ以降のmodel.call()では、自動的にtokenが付いた形でリクエストが送られることになります。それ以外のクライアントの処理は前回のものと同じです。


　次にサーバ側を見ましょう。

```js:index.js
var falcorExpress = require('falcor-express');
var Router = require('falcor-router');
var jsonGraph = require('falcor-json-graph');
var $ref = jsonGraph.ref;
var $error = jsonGraph.error;

var express = require('express');
var app = express();
var bodyParser = require('body-parser'); // call(POST)メソッド処理のために必要
app.use(bodyParser.urlencoded({extended: false}));

//---------- 以下はMongoDB & Mongooseのお決まりの設定です
var mongoose = require('mongoose');
var databaseUri = 'mongodb://localhost/qiita5db';
mongoose.Promise = global.Promise;
mongoose.connect(databaseUri, { useMongoClient: true })
const articleSchema = {
  title:String,
  content:String
};

const Article = mongoose.model('Article', articleSchema, 'articles');
//----------

app.use('/model.json', falcorExpress.dataSourceRoute(function (req, res) {

  // クライアントから送信されたJWT tokenを取得
  var currentToken = req.headers.token;
  console.log("### JWT token = "+currentToken); // ★①

  return new Router([
//================== call(['login'],[credentials])
    {
      route: ['login'] ,
      call: (callPath, args) => {
          /* let { username, password } = args[0]; // === credentials
           * ここではまじめな認証は行わず、JWTもダミーを返す
           */

          return [ { path: ['login', 'token'], value: '0123456789' }, // ダミーtoken
                   { path: ['login', 'username'], value: args[0].username },
                   { path: ['login', 'error'], value: false }
          ];
       }
    },


//================== get("articles.length")
    {
      route: 'articles.length',
      get: () => { return  Article.count({}, (err, count) => count)
        .then ((articlesCount) => {
          return {
            path: ['articles', 'length'],
            value: articlesCount
          };
        })
      }
    },

//================== call( 'articles.add',[newArticle] )
    {
      route: 'articles.add',
      call: (callPath, args) => { //callの場合はargsにクライアント側の引数[newArticle]が渡される

          // JWT tokenを確認してクライアント（現ユーザが）がaddの権利があるかどうかを確認します。
          if(currentToken !== '0123456789') {
            return { // 今回はerror処理は適当です
              path: ['articles'],
              value: $error('auth error')
            }
          }

          var newArticleObj = args[0];
          var article = new Article(newArticleObj);

          return article.save(function (err, data) {
              if (err) {
                console.info("ERROR", err);
                return err;
              }
              else {
                return data;
              }
          }).then ((data) => {
              return Article.count({}, function(err, count) {
                  }).then((count) => {
                    return { count, data };
                  });
          }).then ((res) => {
            var newArticleDetail = res.data.toObject();
            var newArticleID = String(newArticleDetail["_id"]);
            var NewArticleRef = $ref(['articlesById', newArticleID]); // 実体へのシンボリックリンク
            
            // addによってJSON Graphは副作用を受けます。resultsによって結果を返すだけでなくキャッシュを更新します
            var results = [
              {
                path: ['articles', res.count-1], // JSON Graphのarticles配列の最後に新article（の$ref）を追加
                value: NewArticleRef
              },
              {
                path: ['articles', 'newArticleID'], // addの結果として追加されたarticleの_idを返す
                value: newArticleID
              },
              {
                path: ['articles', 'length'], // addの影響を反映させるためcacheのlengthを更新する
                value: res.count
              }
            ];

          return results;
        });

      }
    },

//================== get(['articlesById',[newArticleID],['_id','title', 'content']])
// articlesByIdがarticle実体の定義になります
   {
      route: 'articlesById[{keys}]["_id","title","content"]',
      get: function(pathSet) {
        var articlesIDs = pathSet[1];
        return Article.find({ // ハンドラはMongoose Promiseを返す
              '_id': { $in: articlesIDs} // DBへの１回のアクセスで必要な全てを配列としてget
          }, function(err, articlesDocs) {
            return articlesDocs;
          }).then ((articlesArray) => {
            var results = [];

            articlesArray.map((articleObject) => { // getした配列をmapで処理
              var articleResObj = articleObject.toObject(); // Mongoose Object を normal Objectに変換
              var idString = String(articleResObj['_id']);

              results.push({
                path: ['articlesById', idString],
                value: articleResObj
              });
            });
            return results;
          });
      }
    },


  ]);
}));

// serve static files from current directory
app.use(express.static(__dirname + '/'));

var server = app.listen(3000);
```

　サーバ側ではパス route: ['login']のcall()処理を追加します。
ここでは送られてきた、ユーザ名／パスワードの認証を行い、tokenを生成してクライアントに送ります。この場合、話を簡単にするために、認証は常に成功するダミーで、tokenもダミーを返しています。returnの具体的な内容に注目してください。

```json:
{ path: ['login', 'token'], value: '0123456789' }
```

　ですからクライアントはmodel.getValue('login.token')でtoken値にアクセスできます。

　ログインが成功すると、クライアントは次からreq.headers.tokenにtoken値をセットしてリクエストしてきます。サーバ側は以下のコードでそれを取得します。

```js:
  var currentToken = req.headers.token;
```

　これ以降のアクセス認証がひつようとなる処理に関してはこのtokenをみて yes/no を判断します。

```js:
 if(currentToken !== '0123456789') {
```

 以下がサーバ側の実行結果です（★①）

```
[sand@www13134uf falcor-qiita5]$ node index.js
### JWT token = undefined     <==ログイン要求時のリクエストにはtokenはない
### JWT token = 0123456789    <==ログイン成功後のリクエストにはtokenが付いてくる
### JWT token = 0123456789
### JWT token = 0123456789
```

　クライアント側のブラウザの実行結果は以下のようになります。

```
addの前{ "json": { "articles": { "length": 2 } } }
{ "json": { "articles": { "2": [ "articlesById", "59891ac639064a303d34d73c" ], "length": 3, "newArticleID": "59891ac639064a303d34d73c" } } }
59891ac639064a303d34d73c
{ "json": { "articlesById": { "59891ac639064a303d34d73c": { "_id": "59891ac639064a303d34d73c", "title": "テスト１", "content": "これはテスト１のコンテンツです" } } } }
addの後{ "json": { "articles": { "length": 3 } } }
```


　サーバ側の環境作成は以下の通りです

```sh:
mkdir falcor-qiita5
cd falcor-qiita5
npm init

npm install express --save
npm install falcor-express --save
npm install falcor-router --save
npm install mongoose --save

npm install body-parser --save
npm install falcor-json-graph --save

mongoimport --db qiita5db --collection articles --jsonArray initData.js --host=127.0.0.1

node index.js
```


初期データは以下の通りです。

```js:initData.js
[
  {
    title: '夏休み１日目',
    content: '１日目は東武動物公園のプールに行った'
  },
  {
    title: '夏休み２日目',
    content: '２日目は森林公園に行ってセミをとった'
  }
]
```



最後にpackage.jsonを掲載しておきます

```json:package.json
{
  "name": "falcor-qiita4",
  "version": "1.0.0",
  "description": "mkdir falcor-qiita3\r cd falcor-qiita3\r npm init",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.17.2",
    "express": "^4.15.3",
    "falcor-express": "^0.1.4",
    "falcor-json-graph": "^1.1.7",
    "falcor-router": "^0.8.1",
    "mongoose": "^4.11.5"
  }
}
```