Express + passport(-twitter) + (PM2)で最速でTwitter認証機能付きWebAppを作る

いくつかTwitter認証を使うWebApplicationを作る過程でTwitterでの認証をテンプレート化させコピペで作れるようにしたのでそれをご紹介します。

# この記事で出来るようになること

- Twitter認証機能を持ったExpressAppをサクッと作れるようになる
- WebAppとしても,ネイティブ使うAPIエンドポイントとしても使える
- (後日出来るようになるかも) Swift4でのiOSネイティブからの当APIの使い方がサクッとわかる


# 前提
コピペで作るためには以下を満たしておく必要があります。

- TwitterAppを作成できる
    - 以前にTwitterDeveloperの大規模な変更があって、TwitterAPPを作るためには作文を書かなきゃいけなかった気がします。
    - [TwitterDeveloper](https:/developer.twitter.com/en/apps)にアクセスしてAppのリストとCreate an app ボタンが表示されればOK.

- express-generatorが導入されている
    Expressプロジェクトをコマンド一つで生成してくれる便利な奴
    [導入とか(公式)](https://expressjs.com/ja/starter/generator.html)
    
- PM2が導入されている
    ProcessManager(多分)
    nodeプロジェクトを手軽にデーモン化させられる奴（起動/ホットリロード/クラッシュ時の再起動/startup等々)
    [導入とか(公式)](http://pm2.keymetrics.io/)
    
- ドメインが用意されていてWebAppを公開できるようになっている
    長くなるので省きますが、https://xxx.your.domain/ とかにアクセスして生成したてのExpressを起動した際に
    
        Express
        Welcome to Express
        
    が表示されればOK.
    
    
今回は`https://qiita.your.domain/`というURLでAPIを公開してみたいと思います。
   
# TwitterAppを作成
1. [twitterDeveloper](https://developer.twitter.com/en/apps)からCreate an appを押し、App作成画面へ移ります。

2. 必要事項を記述し、`Allow this application to be used to sign in with Twitter`項目の`Enable Sign in with Twitter`にチェックを入れます。
3. `Callback URLs`項目がアクティブになるので、ここに`https://qiita.your.domain/auth/twitter/callback`を入力します。
    ※ /auth/twitter/callbackの部分は本来任意ですが、これを変更する場合後述のルーティングのコードも変更する必要があります。
4. 項目記述後、流れに沿ってAppを作成し最終的に自分のAppsDashboardに作成したAppが表示されていれば準備完了。
5. AppsDashboardからAppを開き、keys and tokensのConsumer API Keysを確認できるようにしておきます。

   
# 実装

## Express
まずはExpressを初期化します。

```console
$ express -e qiita-twitter
$ cd qiita-twitter
$ npm i
```

次にpm2を初期化します

```console
$ pm2 init
$ vi ecosystem.config.js
```

## pm2
pm2の設定を変更します。

```js:ecosystem.config.js
module.exports = {
    apps: [
        {
            name: 'qiita-twitter',
            script: 'npm',
            args: 'start',
            watch: true,
            env: {
                PORT: 3000,
                HOST_NAME: 'qiita.your.domain',
                USE_SSL: true,
                AUTH: {
                    twitter: {
                        active: true,
                        CONSUMER_KEY: 'Your CONSUMER_KEY',
                        CONSUMER_SECRET: 'Your CONSUMER_SECRET',
                    },
                }
            }
        }
    ]
};
```

nameはpm2上で管理するApp名です。
`pm2 logs qiita-twitter`とかするとログを見れたりします。

scriptとargsは起動時のコマンドです。
`$ npm start`で起動するので、このように記述します。

watchはホットリロード(ファイルを変更すると自動再起動してくれるやつ)を使うか否かです。
trueにすると起動ディレクトリ以下全てのファイルを、trueの代わりに['routes', 'models']みたいにするとroutesとmodelsディレクトリ以下を監視してくれるようです。

で、重要なのがenvです。

USE_SSLとかは後ほどコールバックURLを組み立てるときに使っていますが、そこでURLを直打ちしてしまえば必要ありません。

AUTH=>twitter=>activeみたいになってるのは、他にもlocal認証（よくあるメアドとPWの認証)やFaceBook認証とか使う時に分かるやすくなるメリットがあります。

この辺はコードと相談しながらお好みで変更しましょう。

**gitで管理する場合ecosystem.config.jsはgitignoreに突っ込むなりしてgit監視下から外しましょう**

個人的にはecosystem.config.js.sampleというenvの値を空白にしたものを用意しておき、これをgitに上げておくのがベターかなと思います。


## 休憩

一先ずここで起動確認をしておきましょう。

``` console
$ pm2 start ecosystem.config.js
$ pm2 logs qiita-twitter
```

適当なブラウザで`https://qiita.your.domain`にアクセスして

    Express
    Welcome to Experss
    
が表示されていればOKです。

## Twitter認証

いよいよTwitter認証のroutingを実装します。

以下個人的に使っているディレクトリ構成となります。
お試す前にコードを読んで各自改変することをお勧めします。
(contorllers内でrouting設定しちゃってたりと健康被害を及ぼす可能性があります）

### packageインストール

```console
$ npm i -S express-session passport passport-twitter
```

### ルーティング

```console
$ mkdir controllers
$ touch controllers/authController.js
$ touch controllers/userController.js
```

```js:controllers/authController.js

const passport = require('passport');
const TwitterStrategy = require('passport-twitter').Strategy;
const uc = require('./userController');

module.exports = { 
    initialize: function(app) {
        this.app = app;
        this.authSettings = JSON.parse(process.env.AUTH);

        app.use(passport.initialize());
        app.use(passport.session());
        passport.serializeUser(uc.userSerialize);
        passport.deserializeUser(uc.userDeserialize);

        this.twitterActivate();
    },
    twitterActivate: function() {
        if(!this.authSettings.twitter.active) {
            console.log('Twitter Auth is not activated');
            return;
        }
        let consumerKey = this.authSettings.twitter.CONSUMER_KEY;
        let consumerSecret = this.authSettings.twitter.CONSUMER_SECRET;
        let callbackURL = process.env.USE_SSL ? 'https' : 'http';
        callbackURL += '://';
        callbackURL += process.env.HOST_NAME;
        callbackURL += callbackURL.endsWith('/') ? '' : '/';
        callbackURL += 'auth/twitter/callback';
        passport.use(new TwitterStrategy({
            consumerKey: consumerKey,
            consumerSecret: consumerSecret,
            callbackURL: callbackURL
        }, uc.twitterAuth));

        this.app.get('/auth/twitter', passport.authenticate('twitter'));
        this.app.get('/auth/twitter/callback', passport.authenticate('twitter'), async function(req, res) {
            const user = req.session.passport.user;
            res.json(user);
        });
        console.log('Twitter Auth is activated');
    },
}
```

```js:controllers/userController.js

const ac = require('./authController');

module.exports = {
    twitterAuth: async function(token, tokenSecret, profile, done) {
        //本来ここでユーザー検索して、存在したらそのObjectを返す
        //存在しなければ新規ユーザー作成
        //みたいなことをする。
        //tokenとtokenSecretそのままobjectに格納してログイン状態として扱うのは良くないです。
        const userObj = {
            twitterId: profile.id,
            screenName: profile.username,
            token: token,
            tokenSecret: tokenSecret
        };
        
        //ログ表示もいくない。
        console.log(userObj);
        done(null, userObj);
    },
    userSerialize: async function(user, done) {
        done(null, user);
    },
    userDeserialize: async function(id, done) {
        console.log('user deserializing');
        console.log(id);
        done(null, id);
    },
};
```

## app.jsから読み込み/初期化

```console
$ vi app.js
```

最後にapp.jsを編集し、読み込み・初期化を行います。

```js:app.js
//最魚のrequire群でsessionとauthControllerを読み込み（6行目くらい)
const session = require('express-session');
const authController = require('./controllers/authController');

//requireしたモジュール系をuseしている部分でsessionの読み込みをしてからauthControllerをinitialize(順番大事)を追加(23行目くらい)
app.use(session({
    secret: 'kokoikeapi',
    resave: false,
    saveUninitialized: true
}));
authController.initialize(app);

```

# 動作確認

最後にコールバックしてきたら、ユーザーのTweet一覧を表示してみましょう.

さらに体に良くないですが、authController内でTwitterモジュールを読み込んでお試し表示してみましょう。

```console
$ npm i -S twitter
```

```js:controllers/authController.js

//4行目くらいでrequireしておきます。
const Twitter = require('twitter');

// 36行目くらいの/auth/twitter/callbackのルーティングの処理の中身を変更
 const user = req.session.passport.user;
 const consumer = JSON.parse(process.env.AUTH).twitter;
 const client = new Twitter({
     consumer_key: consumer.CONSUMER_KEY,
     consumer_secret: consumer.CONSUMER_SECRET,
     access_token_key: user.token,
     access_token_secret: user.tokenSecret
 });

const params = { screen_name: user.screenName};
client.get('statuses/user_timeline', params, function(err, tweets, response) {
    if(!err) {
        res.json(tweets);
    } else {
        res.json(err);
    }
});

//res.json(user);
```

これで認証が終わってcallbackしてくると自分のツイート一覧のjsonがresponseに流れます。


本来の使い方としては、userController時点でuserデータを作成し、doneにそのuserデータを渡しておきます。
すると、authControllerで取得するuserにはidが含まれているので、これを使ってユーザーのプロフィールページやトップページにリダイレクトするといいと思います。

```js:controllers/authController.js

this.app.get('/auth/twitter/callback', passport.authenticate('twitter'), async function(req, res) {
    //トップへリダイレクトする場合
    //res.redirect('/');
   
   //user個別ページへリダイレクトする
   const user = req.session.passport.user;
   res.redirect('/users/' + user.id);
   
   //みたいな。
});
```