AngularJS+NodeJS+MongoDB+OAuthでWebサービスを作る

JavaScriptの勉強を兼ねて、Webサービスの作り方まとめです。
簡単なTodoリストを作ります。

# AngularJS+NodeJS(ExpressJS)+MongoDB
Webサービスを作るには色々と手段はありますが、今回選んだのは上記の組み合わせ。
サーバからクライアントまで全部JavaScriptで書けるためです。
MongoDBのサイトでは、[MEANスタック](http://jp.blog.mongodb.org/post/49262866911/the-mean-stack-mongodb-expressjs-angularjs-and)などと呼ばれている模様です。


以下、meanstack-sampleと名付けて、動くようになるまでにやったことを順番に書いていきます。

1. [Yomanの設定](#yeoman)
2. [NodeJSでのREST](#rest)
3. [MongoDBのインストールとNodeJSとの接続](#mongo)
4. [AngularJSとRESTの連携](#angular)
5. [NodeJSでのOAuth認証](#oauth)
6. [まとめと参考](#matome)

## <a name="yeoman"></a>Yeomanの設定
[別エントリ参照](http://torub.github.io/blog/yeoman.html)

## <a name="rest"></a>NodeJS(+ExpressJS)でRESTもどきを作る
まずはサーバ側が無いと動かないので、
Node.jsでRESTっぽく値を返してくれるものを準備します。

Node.jsのWebアプリケーションフレームワークであるExpressをインストールします。
詳しいガイドは[こちら](http://hideyukisaito.github.io/expressjs-doc_ja/guide/)。

```
% npm install express
```

Node.js/Expressを使って以下をweb.jsとして作成します。

```js
var express = require('express'),
  http = require('http'),
  path = require('path'),
  db = require('./db.js'),
  application_root = __dirname;

var app = express();
app.configure(function() {
  app.set('port', process.env.PORT || 3000);
  app.set('view engine', 'ejs');

  // AngularJSのディレクトリを静的ファイルとして追加
  app.use(express.static(path.join(application_root, "app")));

  app.use(express.cookieParser());
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
});

app.get('/todo', db.read); // GETの処理
app.post('/todo', db.create); // POSTの処理
app.delete('/todo/:id', db.delete); // DELETEの処理
app.put('/todo/:id', db.update); // PUTの処理

// サーバ起動
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});
```

URLを定めて、HTTPのGET、POST、DELETE、PUTを使うことで
[CRUD](http://ja.wikipedia.org/wiki/CRUD)を実現します。
AngularJS側からHTTPメソッドを使うことで、簡単にデータ操作ができるようになります。

## <a name="mongo"></a>MongoDBのインストールとNodeJSとの接続
サーバ側の仕組みが作れたので、次はDBと接続します。
同じJavaScriptで、ということでMongoDBを選んでみました。
MongoDBは[Wikipedia](http://ja.wikipedia.org/wiki/MongoDB)によると、

>オープンソースのドキュメント指向データベースである。C++言語で記述されており、開発とサポートはMongoDB Inc.によって行なわれている。MongoDBはRDBMSではなく、いわゆるNoSQLと呼ばれるデータベースに分類されるものである。

というもので、SELECT文等のSQLを使わず、JavaScriptのメソッドでデータ操作をします。
インストール等の準備は、[ここ](http://codezine.jp/article/detail/6982)を見ればできます。

Node.jsからMongoDBに接続するためには、色々なライブラリが存在しています。
ぐぐってみると、mongooseというものが有名なようですが、
今回はより単純な[mongodb](http://mongodb.github.com/node-mongodb-native/)というライブラリを使ってみます。
これは、ターミナルからMongoDBを使う場合とほぼ同じ感じで使えるみたいです。

```
% npm install mongodb
```

でインストールして、web.js内で利用しているdb.jsで接続部分を作ります。

```js
var mongo = require('mongodb');
var Db = mongo.Db,
  BSON = mongo.BSONPure;

var mongoUri = 'mongodb://localhost/contentdb';

// CRUDのC
exports.create = function(req, res) {
  var content = JSON.parse(req.body.mydata);
  Db.connect(mongoUri, function(err, db) {
    db.collection('todolist', function(err, collection) {
      collection.insert(content, {safe: true}, function(err, result) {
        res.send();
        db.close();
      });
    });
  });
};

// CRUDのR
exports.read = function(req, res) {
  Db.connect(mongoUri, function(err, db) {
    db.collection('todolist', function(err, collection) {
      collection.find({}).toArray(function(err, items) {
        res.send(items);
        db.close();
      });
    });
  });
};

// CRUDのU
exports.update = function(req, res) {
  var content = JSON.parse(req.body.mydata);
  var updatedata = {};
  updatedata.data = content.data;
  updatedata.checked = content.checked;
  var id = req.params.id;

  Db.connect(mongoUri, function(err, db) {
    db.collection('todolist', function(err, collection) {
      collection.update({'_id':new BSON.ObjectID(id)}, updatedata, {upsert:true}, function(err, result) {
        res.send();
        db.close();
      });
    });
  });
};

// CRUDのD
exports.delete = function(req, res) {
  var id = req.params.id;
  console.log(id);

  Db.connect(mongoUri, function(err, db) { // add Db.connect
    db.collection('todolist', function(err, collection) {
      collection.remove({'_id':new BSON.ObjectID(id)}, {safe:true}, function(err, result) {
        res.send();
        db.close();
      });
    });
  });
};
```


基本はDBに接続、コレクションを選択、コレクションを操作、DBクローズの流れで処理をします。
毎回DBに接続せずにコネクションを保持するやりかたもあると思いますが、
Heroku上で動かそうとしたら上手く行かず、この形式にしました。


## <a name="angular"></a>AngularJSとRESTの連携
ここまでくればサーバ側は完成しているので、後はクライアント側をAngularで作っていきます。
yeomanを使ってyo angular後の状態として編集をしていきます。

app/views/main.htmlと、app/scripts/controllers/main.jsを修正します。
画面は簡単なTODOリストです。(何故かHTML内の中括弧が表示できないので、全角にしています。)

```html
<div class="hero-unit">
  <input type="text" ng-model="newtodo"><input type="button" value="add todo" ng-click="createTodo()">
  <ul>
      <li ng-repeat="todo in todolist">
        <input type="checkbox"  ng-change="updateTodo(todo)" ng-model="todo.checked">
        <span class="done-｛｛todo.checked｝｝">｛｛todo.data｝｝</span>
        <input type="button" value="×" ng-click="deleteTodo(todo)">
      </li>
  </ul>
</div>
```

```js
'use strict';

angular.module('angularApp').controller('MainCtrl', function ($scope, $http) {//, $templateCache) {
  var url = '/todo';

  $scope.todolist = [];
  $scope.getTodo = function() {
    $http.get(url).success(function(data) {
      $scope.todolist = data;
    });
  };

  $scope.createTodo = function() {
    var todo = {};
    todo.checked = false;
    todo.data = this.newtodo;

    var senddata = 'mydata=' + JSON.stringify(todo);
    $http({
      method: 'POST',
      url: url,
      data: senddata,
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    }).success(function(response) {
      $scope.getTodo();
    }).error(function(response) {
    });
    this.newtodo = "";
  };

  $scope.updateTodo = function(todo) {
    var senddata = 'mydata=' + JSON.stringify(todo);
    $http({
      method: 'PUT',
      url: url + '/' + todo._id,
      data: senddata,
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    }).success(function(response) {
      $scope.getTodo();
    }).error(function(response) {
    });
  };

  $scope.deleteTodo = function(todo) {
    var senddata = 'mydata=' + JSON.stringify(todo);
    console.log(senddata);
    $http({
      method: 'DELETE',
      url: url + '/' + todo._id,
      data: senddata,
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    }).success(function(response) {
      $scope.getTodo();
    }).error(function(response) {
    });
  };

  $scope.getTodo();
});
```

ここまででひととおり動くようになったので、ここまでのソースを[github](https://github.com/torub/meanstack-sample/tree/708c5d94bdf40a784c41f7948c9f9a834c97b4c8)に置きました。

動かすと、以下のような画面になります。

![todo.png](https://qiita-image-store.s3.amazonaws.com/0/32706/913b9783-a3cd-2607-b4d7-bc6654c42786.png)


## <a name="oauth"></a>NodeJSでのOAuth認証
ここまで作成したものに認証機能を付けていきます。
修正箇所は主に以下の3点です。

- Node.jsのPassportライブラリを利用して、認証機能追加(web.js修正)
- データベースの項目にuidを追加(db.js修正)
- クライアント側のapp.jsを修正、login.html追加

### web.js修正

Node.jsのPassportというライブラリを使うと、簡単に様々な認証機能を持たせることができます。今回は[Passportの公式サイト](http://passportjs.org/guide/twitter/)を参考に、Twitter認証を追加します。

新たにauthという関数を用意して、認証が通っているかを確認した上で、認証の上でDB操作を行う用に変更します。
なお、consumerKeyや、consumerSecretは、Twitterの[Developerのページ](https://dev.twitter.com/apps)から取得できます。

```js
/* passportの設定 */
var passport = require('passport'),
  TwitterStrategy = require('passport-twitter').Strategy;

// Passport: TwitterのOAuth設定
passport.use(new TwitterStrategy({
  consumerKey: "your key",
  consumerSecret: "your secret",
  callbackURL: "/auth/twitter/callback"
}, function(token, tokenSecret, profile, done) {
  // ユーザIDを設定
  profile.uid = profile.provider + profile.id;
  process.nextTick(function() {
    return done(null, profile);
  });
}));

// Serialized and deserialized methods when got from session
passport.serializeUser(function(user, done) {
  done(null, user);
});

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

// Define a middleware function to be used for every secured routes
// 認証が通っていないところは401を返す
var auth = function(req, res, next){
  if (!req.isAuthenticated()) {
    res.send(401);
  } else {
    next();
  }
};


var app = express();
app.configure(function() {
  ...

  // OAuth認証用
  app.use(express.session({ secret: 'hogehoge', cookie: {maxAge: 1000 * 60 * 60 * 24 * 30} }));
  app.use(passport.initialize()); // Add passport initialization
  app.use(passport.session());    // Add passport initialization

  app.use(app.router);
});

// route to log in
app.get("/auth/twitter", passport.authenticate('twitter'));
app.get("/auth/twitter/callback", passport.authenticate('twitter', {
  successRedirect: '/',
  falureRedirect: '/login'
}));

app.get('/todo', auth, db.read); // GETの処理。要認証(auth関数の後にこれまでの処理実施)
...
```

### db.jsの修正
ユーザ毎にデータを持つため、MongoDBのCRUD操作の際にuidを追加します。

```js
...

// CRUDのC
// セッション内のUIDに紐付けてデータを作成する
exports.create = function(req, res) {
  var uid = req.user.uid;
  var content = JSON.parse(req.body.mydata);
  content.uid = uid;
  Db.connect(mongoUri, function(err, db) {
    db.collection('todolist', function(err, collection) {
      collection.insert(content, {safe: true}, function(err, result) {
        res.send();
        db.close();
      });
    });
  });
};

...
```


### app.jsの修正(とlogin.html追加)
web.jsのauth関数と連動して、認証されていない際に帰ってくる401エラーを検知する機能を実現します。
HTTPレスポンスを確認して、401エラーが含まれていればログインページに飛ばす処理を作ります。

[AngularJS公式の$httpページ](http://code.angularjs.org/1.0.8/docs/api/ng.$http)を参考に作りました。
が、使う前に「$q and deferred/promise APIs」の理解が必要と記載があり、そこが理解できていないため、
過不足があるかもしれません。


```js
...
// []で囲わないと、gruntでminifyした際にエラーとなる
$httpProvider.responseInterceptors.push(['$q', '$location',function($q, $location) {
  return function(promise) {
    return promise.then(function(response) {
        // Success: 成功時はそのまま返す
        return response;
      }, function(response) {
        // Error: エラー時は401エラーならば/loginに遷移
        if (response.status === 401) {
          $location.url('/login');
        }
        return $q.reject(response);
      }
    );
  };
}]);
...
```

```html
<div class="container">
  <div class="page-header">
    login
  </div>

  <div>
    <p><button class="btn-auth btn-twitter" onclick="location.href='/auth/twitter'">
      Sign in with <b>Twitter</b></button></p>
  </div>
</div>
```



## <a name="matome"></a>まとめと参考
認証を追加したところまでのソースコードは[github](https://github.com/torub/meanstack-sample/tree/1c47d5b917da08590030757a547bd14b8f0ad351)に置いてあります。見た目もしょぼく、セキュリティ面でも怪しいところはありますが、一通り動作するはずです。

あとは最初から入れてあるBootStrapを使って見た目を直すとか、Herokuにアップしてみるとか、
Facebookでの認証も付けてみるとか、色々応用ができると思います。

### 参考
- [DailyJSのAngularJS回](http://dailyjs.com/2013/04/11/angularjs-1/)
- [日々之スクラッチ: Node.js + express + MongoDB on Heroku でアプリケーションを作成する](http://sctfarch.blogspot.jp/2012/03/nodejs-express-mongodb-on-heroku.html)
- [チュートリアル: node.jsとHerokuでAngularJSアプリを作る Part I ](http://techblog.appirio.co.jp/2013/02/nodejsherokuangularjs-part-i.html)
- [【Node.js】PassportでFacebookとTwitterのOAuth認証する方法 | creator note](http://creator.cotapon.org/articles/node-js/node_js-oauth-passport-facebook-twitter)
- [SQL脳に優しいMongoDBクエリー入門 - taka512's blog](http://taka512.hatenablog.com/entry/20110220/1298195574)