0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Netflix Falcorについて(5)

Last updated at Posted at 2017-08-08

Netflix Falcorについて(1)
Netflix Falcorについて(2)
Netflix Falcorについて(3)
Netflix Falcorについて(4)
Netflix Falcorについて(5)

 今回は、前回のプログラムに認証の枠組みを加えます。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では第2引数に、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: "テスト1",
            content: "これはテスト1のコンテンツです"
        };
        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()の第2引数にheaderオプションとしてtokenを指定してmodelを作成します。それ以降のmodel.call()では、自動的にtokenが付いた形でリクエストが送られることになります。それ以外のクライアントの処理は前回のものと同じです。

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

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への1回のアクセスで必要な全てを配列として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の具体的な内容に注目してください。

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

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

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

  var currentToken = req.headers.token;

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

 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": "テスト1", "content": "これはテスト1のコンテンツです" } } } }
addの後{ "json": { "articles": { "length": 3 } } }

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

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

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

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

最後にpackage.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"
  }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?