node.js + express-generator + passportでJawbone UPフィットネスデータをグラフ表示

前回、[express-generatorでWebアプリのひな型を作成](http://7nohe.github.io/getting-started-with-express.html)するところまでやりましたが、今回はフィットネスデバイスJawbone UPから得たデータをWeb上で表示させるとこまでやりたいと思います。  

コードは[Github](https://github.com/7nohe/jawbone-web-app)で公開しています。  




# Jawbone アカウント作成、アプリケーション作成

まず最初に、[Jawbone UP Developerサイト](https://jawbone.com/up/developer)へ行って、"Create an Account"をクリックしてアカウント作成します。  

アカウントができたら、"Create App"をクリック、これから作成するアプリケーションを登録します。  
以下の情報を入力します。  

- Name （アプリケーション名）
- Description　（説明文）
- Long Description　（説明長文）
- Logo　（ロゴ）
- URL　（アプリホームページ）
- Authorization URL　（ログインページ）
- OAuth Redirect URIs　（認証後リダイレクトページ）

アプリを作成し終えたら次の情報をメモしときます。  

- Client ID
- App Secret

これらはユーザー認証のときに必要になります。  





# アプリケーション準備

前回行ったようにexpress-generatorでアプリのひな型を作成します。  

作成し終えたら、必要なモジュールをインストールします。  
package.jsonのdependeciesは以下のようです。  



```
"dependencies": {
    "body-parser": "~1.13.2",
    "cookie-parser": "~1.3.5",
    "debug": "~2.2.0",
    "dotenv": "^2.0.0",
    "ejs": "~2.3.3",
    "express": "~4.13.1",
    "express-session": "^1.13.0",
    "jawbone-up": "^0.1.2",
    "morgan": "~1.6.1",
    "passport": "^0.3.2",
    "passport-local": "^1.0.0",
    "passport-oauth": "^1.0.0",
    "serve-favicon": "~2.3.0"
  }
```

簡単に主要なモジュールだけの説明をすると  

ejsはテンプレートエンジン用、
body-parserはjsonリクエストを扱うため、
express-sessionはセッションを使えるように、
dotenvで開発環境での.envファイルから環境変数読み込むためといった感じです。  

ここで今回重要になるのがpassportとjawbone-upの二つです。

PassportはfacebookやTwitterなどのサービスのユーザー認証を簡単に行えるようにできるモジュールです。他にもローカルやカスタムストラテジーにも対応しています。  


jawbone-upはjawbone apiコールを簡単に行えるようにするモジュールです。  


# 設定

それぞれインストールしたモジュールをrequireして使えるようにします。

Jawboneアカウントページから作成したアプリのClient IDとApp Secretを直接書き込むのはよくないのでアプリディレクトリ以下に.envファイルをつくり、githubなどへ公開するときは.gitignoreに.envを入れればOKです。
そこへ  



```
AWBONE_CLIENT_ID="取得したClient ID"
JAWBONE_CLIENT_SECRET="取得したApp Secret"
```


として保存します。  

そして  

```
var dotenv = require('dotenv');
dotenv.load();
```


これで`process.env.JAWBONE_CLIENT_ID`と`process.env.JAWBONE_CLIENT_SECRET`でそれぞれ値を取得できます。  

app.jsの設定部分は以下のようになりました。  


passportで認証を行ったとき取得したアクセストークンなどをセッションに保存したいので、passportでのセッションを有効にするにはexpress-sessionモジュールを使う必要があります。  

そしてpassport.initialize()のあとpassport.session()で使えるようになります。
sessionのシークレットキーはなんでもいいです。  




```
var passport = require('passport'),
session = require("express-session");
.
.
app.use(session({
  secret: 'secret key'
}));
app.use(passport.initialize()); 
app.use(passport.session()); 

```


認証成功後のコールバックURLを用意します。  
今回は本番用と開発用に分けています。  


```
var callbackURL = app.get('env') === 'production' ? "本番環境用コールバックURL" : "開発環境用コールバックURL";
```

OAuth2認証を行いたいのでpassport-oauthをつかってストラテジーを用意。  
先ほどのURLとJawboneサービス認証URLとトークンURLが認証の時さらに必要になります。  


```
var JawboneStrategy = require('passport-oauth').OAuth2Strategy,
jawboneAuth = {
  authorizationURL: 'https://jawbone.com/auth/oauth2/auth',
  tokenURL: 'https://jawbone.com/auth/oauth2/token',
  callbackURL: callbackURL
};
```




こんな感じで全体的な設定は以下のようになりました。  




```
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var users = require('./routes/users');
var dotenv = require('dotenv');

//read environment variable
dotenv.load();


var passport = require('passport'),
session = require("express-session");

var app = express();

//set callback url depending on mode
var callbackURL = app.get('env') === 'production' ? "本番環境用コールバックURL" : "開発環境用コールバックURL";


var JawboneStrategy = require('passport-oauth').OAuth2Strategy,
jawboneAuth = {
  authorizationURL: 'https://jawbone.com/auth/oauth2/auth',
  tokenURL: 'https://jawbone.com/auth/oauth2/token',
  callbackURL: callbackURL
};

app.use(express.static(path.join(__dirname, 'public')));

//share jawboneAuth with other .js file
app.set('jawboneAuth', jawboneAuth);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());


app.use(session({
  secret: 'secret'
}));
app.use(passport.initialize()); 
app.use(passport.session()); 


```





# Jawbone　OAuth2ストラテジー

passportではfacebookやtwitterなどの多数のサービスに対応していますが、Jawboneにはサポートしていません。そこでOAuth2認証のJawbone APIに対してpassport-oauth2モジュールを使います。  
先ほど用意した、  

`var JawboneStrategy = require('passport-oauth').OAuth2Strategy`  

に合わせて認証を行います。  
詳しいことは[Github passport-oauth2](https://github.com/jaredhanson/passport-oauth2)を読んでください。  
以下の値が必要になります。  

- clientID
- clientSecret
- authorizationURL
- tokenURL
- callbackURL

clientIDとclientSecretは事前に用意した.envから、authorizationURL、tokenURL、callbackURLは先ほどjawboneAuthにすべて用意してあります。  


認証の流れとしては、  

ログイン時にJawbone アカウント認証  
        ↓
アクセストークンをセッションに保存  
        ↓  
取得したアクセストークンでjawbone-upモジュール読み込み  
        ↓  
APIコール（ユーザー情報取得など）  


といった流れになります。  

ここでアクセストークンをセッションに保存することで次回には認証を行わずにAPIコールすることが出来ます。  
app.jsに以下のようにOAuth2認証を加えます。  



```
passport.use('jawbone', new JawboneStrategy({
  clientID: process.env.JAWBONE_CLIENT_ID,
  clientSecret: process.env.JAWBONE_CLIENT_SECRET,
  authorizationURL: jawboneAuth.authorizationURL,
  tokenURL: jawboneAuth.tokenURL,
  callbackURL: jawboneAuth.callbackURL
  },
  function(accessToken, refreshToken, profile, done){
    var user = {
      accessToken: accessToken,
      refreshToken: refreshToken
    };
    
    var options = {
        access_token: accessToken,
        client_id: process.env.JAWBONE_CLIENT_ID,
        client_secret: process.env.JAWBONE_CLIENT_SECRET
      },
    up = require('jawbone-up')(options);
    
    
    //get user data
    up.me.get({}, function(err, body){
      if(err) {
        console.log('Error receiving user data');
      }
      else {
        var userdata = JSON.parse(body).data;
        var jawboneData = {
          last: userdata.last,
          first: userdata.first
        };
        
        console.log('Successfully Logged in. Hello, '+ jawboneData.last + ' ' + jawboneData.first + '!')
        done(null, user);
       }
    });//end of user data
}));
```

jawbone-upモジュールについてですが、詳しい内容は[Github node-jawbone-up](https://github.com/ryanseys/node-jawbone-up)を参照してください。  


上のコードでは`up.me.get({}, コールバック関数)`でユーザー情報を取得しています。  
なんのデータが取得できるかはGithubを見てください。  


# セッション管理

passportのセッション管理については`passport.serializeUser()`と`passport.deserializeUser()`でおこないます。  

先ほどのコードのdone()  


```
passport.use('jawbone', new JawboneStrategy({
  .
  .
  .
  
  },
  function(accessToken, refreshToken, profile, done){
    .
    .
    .
    
    
    //get user data
    up.me.get({}, function(err, body){
      if(err) {
        console.log('Error receiving user data');
      }
      else {
       .
       .
       .
       
        done(null, user);// <- これ
       }
    });
}));
```

に値を入れてやり、  





```
passport.serializeUser(function(data, done) {
  done(null, data);
});

passport.deserializeUser(function(data, done) {
  done(null, data);
});
```


上記のようにして、doneの第２引数にいれてやることでreq.session.passport.userから呼び出すことができます。  


以上でapp.jsのコーディングは終わりました。  


# ルーティング

設定や認証の用意はできましたので、それぞれのルートについて書いていきましょう。  

## ホーム

ホームページはすでにログイン済みかどうか判断し、ログインしていなければホームページを表示するだけです。  

```
router.get('/', function(req, res, next) {
  if(!req.isAuthenticated()){
    res.render('pages/index', { 
      title: 'Welcome to Jawbone Web App',
      logoutLink: false
    });
  }else{
    res.redirect("/data");
  }
});
```

passportではreq.isAuthenticated()という関数があり認証済みかどうか判断できます。  




## ログイン

ホームに用意されているログインボタンを押すとログインページに行きアカウント認証します。  
このときscopeでどの情報を読み取りたいか指定します。詳しくは[Authentication](https://jawbone.com/up/developer/authentication)から。  



```
router.get('/login/jawbone', 
  passport.authenticate('jawbone', {
    scope: ['basic_read','sleep_read', 'move_read'],
    failureRedirect: '/'
  })
);
```

このとき"https://jawbone.com/auth/oauth2/auth" へアクセスし認証後、app.jsで指定したコールバックURLへ移動します。  

## コールバックURL

今回はコールバックURLを"https://自分のホームページURL/done" に指定していたので以下のようになります。  



```
router.get('/done', passport.authenticate('jawbone', {
        scope: ['basic_read','sleep_read', 'move_read'],
        failureRedirect: '/'
    }), function(req, res) {
        res.redirect('/data');
    }
);
```

このときapp.jsで作成した認証が行われます。  
ユーザー情報の取得、アクセストークンがセッションへ保存され、次回認証せずにJawboneのデータを取得できます。  

## ログインチェック

ルーターでAPIコールするときログイン済みであるかどうか確認する必要があります。そのための関数を用意します。  


```
var isLoggedin = function(req, res, next){
    if(req.isAuthenticated())
        return next();
    res.redirect("/login/jawbone");
};
```

## フィットネスデータ取得、表示

ログイン完了後、セッションに保存したアクセストークンをつかってSleepとMoveのデータを取得します。  


```
router.get('/data', isLoggedin, function(req, res) {
    var options = {
        access_token: req.session.passport.user.accessToken,
        client_id: process.env.JAWBONE_CLIENT_ID,
        client_secret: process.env.JAWBONE_CLIENT_SECRET
      },
    up = require('jawbone-up')(options),
    jawboneData = {
          sleep: "",
          move: ""
        };
        
    //get sleep data
    up.sleeps.get({}, function(err, body) {
      if (err) {
        console.log('Error receiving sleep data');
      } else {
        var sleepData = JSON.parse(body).data;
        console.dir(sleepData);
        for (var i = 0; i < sleepData.items.length; i++) {
          var date = sleepData.items[i].date.toString(),
              year = date.slice(0,4),
              month = date.slice(4,6),
              day = date.slice(6,8);
  
          sleepData.items[i].date = day + '/' + month + '/' + year;
          sleepData.items[i].title = sleepData.items[i].title.replace('for ', '');
          jawboneData.sleep = sleepData;
        }
        //get move data
         up.moves.get({}, function(err, body) {
            if (err) {
              console.log('Error receiving move data');
            } else {
              var moveData = JSON.parse(body).data;
              
              for (var i = 0; i < moveData.items.length; i++) {
                var date = moveData.items[i].date.toString(),
                    year = date.slice(0,4),
                    month = date.slice(4,6),
                    day = date.slice(6,8);
        
                moveData.items[i].date = day + '/' + month + '/' + year;
                moveData.items[i].title = moveData.items[i].title.replace('for ', '');
                jawboneData.move = moveData;
              }
              res.render('pages/userdata', {title: 'User Data', data: jawboneData, logoutLink: true});
            }
         });
      }
    });
});
```


受け取ったデータをpages/userdata.ejsへ渡します。  


## ログアウト

簡単です。  

```
router.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/');
});
```


ルーティングの方は以上です。  



# Views

## テンプレーティング


今回はホーム(index.ejs)とデータ表示ページ(userdata.ejs)のみですが、せっかくなのでテンプレーティングをやってみます。  

viewsディレクトリ以下にpagesとpartialsというフォルダを作成します。  

まず基本となるindex.ejsをpages下に作成します。  


```
<!DOCTYPE html>
<html lang="ja">
  <head>
    <% include ../partials/head %>
  </head>
  <body class="container">
    <header>
        <% include ../partials/header %>
    </header>
    <main>
      <div class="jumbotron start-content">
        <h1><%= title %></h1>
        <h2>Please login to see your fitness data</h2>
        <a href="/login/jawbone" class="btn btn-primary btn-lg">Login</a>
      </div>
    </main>
    <footer>
        <% include ../partials/footer %>
    </footer>
</html>
```


このように`<% include ../partials/head %>`を追加することでpartialsにあるejsファイルを読み込むことができ、一度書いたコードを使いまわすことができます。head、header、footerタグ内はindex.ejsでもuserdata.ejsでも同じなのでテンプレーティングします。  

titleはルーターから受け取った値です。  


そしたら、patials以下にhead.ejs、header.ejs、footer.ejsを追加します。  



- head.ejs  

```
<meta charset="UTF-8">
<title>Jawbone Web App</title>
<link rel='stylesheet' href='../stylesheets/style.css' />
<!-- CSS (load bootstrap from a CDN) -->
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
```


- header.ejs  

```
<nav class="navbar navbar-default" role="navigation">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="/">
                <span class="glyphicon glyphicon glyphicon-road"></span>
                Jawbone Web App
            </a>
        </div>

        <ul class="nav navbar-nav">
            <li><a href="/">Home</a></li>
            <% if(logoutLink){ %>
                <li><a href="/logout">Logout</a></li>
            <% } %>
        </ul>
      
    </div>
</nav>
```



- footer.ejs  

```
<p class="text-center text-muted">© Copyright 2016 Daiki</p>
```


logoutLinkもルーターから来たもので、ログイン済みならログアウトリンクを表示するようにしています。  

cssファイルはpublic/stylesheets下にstyle.cssを作成してください。  






## CanvasJS

肝心なフィットネスデータを表示するuserdata.ejsですが、CanvasJSというJavaScriptライブラリをつかってグラフとして表示させたいと思います。  

詳しい使い方は[CanvasJS](http://canvasjs.com)を見てください。  

これを使うにはCDN経由で  
`<script src="https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.min.js"></script>`  

を追加すればOKです。  

簡単な使い方としては、  

```
<div id="sleepChart" ></div>

<div id="moveChart" ></div>
```  

とdivタグにidを`加え、  

scriptタグ内でデータ格納、チャート設定、表示のコードを書くといった感じになります。  

userdata.ejsではルーターからSleepとMoveデータが入ったdataという名前で受け取るので、これを使って以下のようにチャートを作りました。  


```
     <script>
          var sleepDataPlot = [];
          
          <% if(data.sleep){ %>
            <% for (var i=0; i<data.sleep.items.length; i++) { %>
              var label = '<%= data.sleep.items[i].date %>';
              var secs = <%= data.sleep.items[i].details.duration %>;
              var hours = (secs/60)/60;
              var sleepData = {label: label, y: hours};
              sleepDataPlot.push(sleepData);
            <% } %>
            
           
            //チャートの生成
            var sleepChart = new CanvasJS.Chart("sleepChart", {
               title: {
                text: "Sleep Data"
              },
                data: [{
                  type: 'column',
                  dataPoints: sleepDataPlot
                }],
                axisX:{ 
                  title: "Date"
                },
                axisY:{
                  title: "Hours"
                }
            });
            sleepChart.render();
          <% }else{ %>
            var element = document.getElementById("sleepChart");
            element.innerHTML = "No Data";
          <% } %>
          
          var moveDataPlot = [];
          <% if(data.move){ %>
            <% for (var i=0; i<data.move.items.length; i++) { %>
              var label = '<%= data.move.items[i].date %>';
              var steps = <%= data.move.items[i].details.steps %>;
              
              var moveData = {label: label, y: steps};
              moveDataPlot.push(moveData);
            <% } %>
            
           console.log(moveDataPlot);
            //チャートの生成
            moveChart = new CanvasJS.Chart("moveChart", {
                title: {
                  text: "Move Data"
                },
                data: [{
                  type: 'column',
                  dataPoints: moveDataPlot
                }],
                axisX:{ 
                    title: "Date"
                },
                axisY:{
                  title: "Steps"
                }
            });
            moveChart.render();
          <% }else{ %>
            var element = document.getElementById("moveChart");
            element.innerHTML = "No Data";
          <% } %>
        </script>
```


これで簡単にグラフ表示できます。  



これで簡単なフィットネスデータをグラフで表示するウェブアプリケーションが完成しました。  


![jawbone_graph.png](https://qiita-image-store.s3.amazonaws.com/0/52622/555c33f5-3b75-0c4c-81a3-b180684edd1c.png)




# herokuへのデプロイ

herokuへデプロイするためには、まずProcfileをアプリケーションディレクト以下に作成する必要があります。  


Procfile  
```
web: npm start --production
```

これでproductionモードでアプリが起動します。  

さらに、.gitignoreで公開したくないファイルをアップロードされないようにします。  
とくに開発環境用の.envはアプリのシークレットキーが保存されているので追加します。  


.gitignore  

```
.env
npm-debug.log
```


これで公開準備が整いました。  

初期化  
`git init`  


ログイン  
`heroku login`  

herokuアプリ作成  
`heroku create`  

herokuへのアップロード  

`git add .`  

`git commit -m "first commit"`  

`git push heroku master`  

`heroku open`  

これでアプロード成功すればherokuで動くはずです。  

作成したアプリは公開しています。  

[Jawbone Web Application](https://jawbone-up-web.herokuapp.com)  

前回node.jsを使い始めたばかりなので間違っているところなどあるかもしれませんがなんとか動かすことができました。とくにJawbone UPという全く有名どころでないAPIだったので調べるのに大変でした。  



だいぶ長くなりましたが今回は以上です。  


参考URL  
- https://jawbone.com/up/developer  
- http://canvasjs.com  
- https://github.com/ryanseys/node-jawbone-up  
- https://www.sitepoint.com/connecting-jawbone-up-api-node-js