読者です 読者をやめる 読者になる 読者になる

freeCodeCampを魔改造してSlackでOAuth2認証を可能にする

freeCodeCamp

f821a35be730fb6cf73d75de5ff76ed0f75d9d6a 時点の staging ブランチを魔改造したよっていう話。

passport-slack のインストール

$ npm install --save passport-slack 

Slack でアプリ作成

https://api.slack.com/apps にアクセスして新規アプリを作成する。 Redirect URI がアプリをサービスする URL に対して作成する必要あり。ローカル用では以下を登録する。

http://localhost:3000/auth/slack/callback

App Credentials から Client IDClient Secret をコピって .env に記述する。

diff

以下の通り。どこかのタイミングで github でしか OAuth 認証をしないようにハードコーディングされているので、 slack にハードコーディング上書きしている。プロフィール画像も決め打ち実装。

diff --git a/common/models/User-Identity.js b/common/models/User-Identity.js
index 962fe0d..6cad516 100644
--- a/common/models/User-Identity.js
+++ b/common/models/User-Identity.js
@@ -72,7 +72,7 @@ export default function(UserIdent) {
           loopback.getModelByType(loopback.User);

         const userObj = options.profileToUser(provider, profile, options);
-        if (getSocialProvider(provider) !== 'github') {
+        if (getSocialProvider(provider) !== 'slack') {
           const err = new Error(createAccountMessage);
           err.userMessage = createAccountMessage;
           err.messageType = 'info';
@@ -130,7 +130,8 @@ export default function(UserIdent) {
       }

       const { profile, provider } = userIdent;
-      const picture = getFirstImageFromProfile(profile);
+      // const picture = getFirstImageFromProfile(profile);
+      const picture = profile && profile.user && profile.user.image_72;

       debug('picture', picture, user.picture);
       // check if picture was found
diff --git a/package.json b/package.json
index 2123144..a352f92 100644
--- a/package.json
+++ b/package.json
@@ -95,6 +95,7 @@
     "passport-linkedin-oauth2": "^1.2.1",
     "passport-local": "^1.0.0",
     "passport-oauth": "^1.0.0",
+    "passport-slack": "0.0.7",
     "passport-twitter": "^1.0.3",
     "pmx": "~0.6.2",
     "react": "~15.4.2",
diff --git a/server/passport-providers.js b/server/passport-providers.js
index 2b8aebf..cc8c796 100644
--- a/server/passport-providers.js
+++ b/server/passport-providers.js
@@ -162,5 +162,32 @@ export default {
     successFlash: [ 'We\'ve updated your profile based ',
                     'on your your GitHub account.'
                   ].join('')
+  },
+  'slack-login': {
+    provider: 'slack',
+    module: 'passport-slack',
+    clientID: process.env.SLACK_ID,
+    clientSecret: process.env.SLACK_SECRET,
+    authPath: '/auth/slack',
+    callbackURL: '/auth/slack/callback',
+    callbackPath: '/auth/slack/callback',
+    successRedirect: successRedirect,
+    failureRedirect: failureRedirect,
+    scope: ['identity.basic', 'identity.email', 'identity.avatar', 'identity.team'],
+    failureFlash: true
+  },
+  'slack-link': {
+    provider: 'slack',
+    module: 'passport-slack',
+    clientID: process.env.SLACK_ID,
+    clientSecret: process.env.SLACK_SECRET,
+    authPath: '/link/slack',
+    callbackURL: '/link/slack/callback',
+    callbackPath: '/link/slack/callback',
+    successRedirect: successRedirect,
+    failureRedirect: linkFailureRedirect,
+    scope: ['identity.basic', 'identity.email', 'identity.avatar', 'identity.team'],
+    link: true,
+    failureFlash: true
   }
 };

.env 追記

SLACK_IDSLACK_SECRET を追記している。

Slack 認証リンク

View のどこかに以下を記述すれば OK

        a.btn.btn-lg.btn-block.btn-social.btn-facebook(href='/auth/slack')
            i.fa.fa-slack
            | Sign in with Slack

LoopBack 入門

LoopBack

門を叩きます。

LoopBack - Node.js framework

インストール

$ npm install -g loopback-cli

アプリケーションの作成

$ lb

     _-----_     
    |       |    ╭──────────────────────────╮
    |--(o)--|    │         LoopBack         │
   `---------´   │ アプリケーションを作成しましょう。 │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |     
   __'.___.'__   
 ´   `  |° ´ Y ` 

なんか出たw

? アプリケーションの名前は何ですか? hello_loopback
? プロジェクトを格納するディレクトリーの名前を入力してください: hello_loopback
   create hello_loopback/
     info 作業ディレクトリーを hello_loopback に変更します

? どのバージョンの LoopBack を使用しますか? 3.x (current)
? どのようなタイプのアプリケーションにしますか? hello-world (A project containing a controller, including a single vanilla Message and a single remote method)
.yo-rc.json の生成中

メニューによっては上下キーで選択もできる。すごい。

いろいろモジュールがインストールされたら最後に以下の出力。

次のステップ:

  アプリケーションのディレクトリーに移動します
    $ cd hello_loopback

  アプリケーションでモデルを作成します
    $ lb model

  アプリケーションを実行します
    $ node .

The API Connect team at IBM happily continues to develop,
support and maintain LoopBack, which is at the core of
API Connect. When your APIs need robust management and
security options, please check out http://ibm.biz/tryAPIC

インストラクションに従ってモデルを作成する。

$ cd hello_loopback
$ lb model
? モデル名を入力します: person
? person を付加するデータ・ソースを選択します: db (memory)
? モデルの基本クラスを選択します PersistedModel
? REST API を介して person を公開しますか? Yes
? カスタム複数形 (REST URL の作成に使用します): people
? 共通モデルですか、あるいはサーバー専用ですか? 共通
では、person プロパティーをいくつか追加しましょう。

完了したら、空のプロパティー名を入力してください。
? プロパティー名: 

モデルのプロパティを入力していく。

? プロパティー名: name
   invoke   loopback:property
? プロパティー・タイプ: string
? 必須 Yes
? デフォルト値 [なしの場合は空白のまま]: 

別の person プロパティーを追加しましょう。
完了したら、空のプロパティー名を入力してください。
? プロパティー名: age
   invoke   loopback:property
? プロパティー・タイプ: number
? 必須 Yes
? デフォルト値 [なしの場合は空白のまま]: 20

別の person プロパティーを追加しましょう。
完了したら、空のプロパティー名を入力してください。
? プロパティー名: 

モデルができたらアプリケーションを起動(!)

$ node .

起動したら http://0.0.0.0:3000/explorer/ にアクセス。

作成したモデルの POST を開いて、適当なデータをPOST。

f:id:kozy4324:20170223181006p:plain

内部で curl とか叩いてくれているっぽい。

f:id:kozy4324:20170223181158p:plain

GET を開いてリクエストすると作成した値が返ってきた。

f:id:kozy4324:20170223181412p:plain

うん、コード書かずに簡単な RESTful API ができてる。なお DB は設定していないのでアプリ再起動でデータは消える。DB設定すれば簡単に永続化もできそう。

モデル作成時のファイル差分

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   common/models/message.json
    modified:   server/middleware.json
    modified:   server/model-config.json

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    common/models/person.js
    common/models/person.json

no changes added to commit (use "git add" and/or "git commit -a")

common/models/ 以下にモデル定義とモデルクラスのソースが出力される。

server/model-config.json に列挙されたモデルの情報が入る。

$ git diff server/model-config.json 
diff --git a/server/model-config.json b/server/model-config.json
index a432664..6b901f0 100644
--- a/server/model-config.json
+++ b/server/model-config.json
@@ -37,5 +37,9 @@
   },
   "Message": {
     "dataSource": null
+  },
+  "person": {
+    "dataSource": "db",
+    "public": true
   }
 }

server/datasources.json で DB の接続情報が書けそうだ。

$ cat server/datasources.json 
{
  "db": {
    "name": "db",
    "connector": "memory"
  }
}

server/server.js がエントリーポイントだろう。また server/boot/root.js で middleware 設定してた。これは loopback-boot 関連なのかな。

$ cat server/boot/root.js 
'use strict';

module.exports = function(server) {
  // Install a `/` route that returns server status
  var router = server.loopback.Router();
  router.get('/', server.loopback.status());
  server.use(router);
};

server/component-config.json/explorer の定義もあるな。

$ cat server/component-config.json 
{
  "loopback-component-explorer": {
    "mountPath": "/explorer"
  }
}

全体のディレクトリ構成

$ ls
client      common      node_modules    package.json    server
  • client にはフロントエンドリソースを格納せよと書いてあった
  • common はモデル情報入っている
  • server にはWebアプリ実装が入っている

いまさらながら Express 入門

Express.js

こいつを調べようと思って、

LoopBack - Node.js framework

Express ベースらしいので、最初はそっちから戯れる。

Hello Express!

$  mkdir hello_express
$  cd hello_express
$  npm init
$  npm install express --save
$ cat app.js 
const express = require('express')
const app = express()

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(3000, () => {
  console.log('Example app listening on port 3000!')
})

reqres は Node 標準 API の HTTP のやつと同じ。

$ node app.js
$ curl http://localhost:3000/

Routing 基本

基本形。

app.METHOD(PATH, HANDLER)

GETとPOSTのルーティング。

app.get('/', (req, res) => {
  res.send('Hello World!')
})
app.post('/', (req, res) => {
  res.send('Got a POST request')
})

Static ファイルの配信

ビルドインの express.static middleware を使う。

app.use(express.static('public'))

特定URLでマッピングするなら、 app.use() の第1引数にパスを指定する。

app.use('/static', express.static('public'))

ファイルの探索はプログラム起動したディレクトリからの相対パスなので、絶対パスで指定するのがよい。

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

404 のハンドリング方法

全ての middleware と route を実行した結果、どれもレスポンスを返せなかった場合に 404 となる。なので一番最後に以下 middleware を定義すればよい。

app.use((req, res, next) => {
  res.status(404).send("Sorry can't find that!")
})

エラーハンドリング

引数4つの middleware でエラーハンドリングとなる。

app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

Routing

この基本形。

app.METHOD(PATH, HANDLER)

METHOD は HTTP メソッドと対応してて、めっちゃ定義されている。

get, post, put, head, delete, options, trace, copy, lock, mkcol,  move, purge, propfind, proppatch, unlock, report, mkactivity, checkout, merge, m-search, notify, subscribe, unsubscribe, patch, search, and connect

app.all() という HTTP メソッドと対応していないやつがあり、HTTP メソッド関係ない。特定パスに対してのみ middleware を使うのに等しいのかな。

app.all('/secret', (req, res, next) => {
  console.log('Accessing the secret section ...')
  next() // pass control to the next handler
})

PATH にはパターンや正規表現が使える。 ?, +, *, (, ) はパターンで処理される。

app.get('/ab?cd', (req, res) => {}) // `/abcd`, `/acd`
app.get('/ab+cd', (req, res) => {}) // `/abcd`, `/abbcd`, `/abbbcd`
app.get('/ab*cd', (req, res) => {}) // `/abcd`, `/abxcd`, `/abRANDOMcd`, `/ab123cd`
app.get('/ab(cd)?e', (req, res) => {}) // `/abe`, `/abcde`
app.get('/ab(cd)?e', (req, res) => {}) // `/abe`, `/abcde`
app.get(/a/, (req, res) => {}) // `a` 含むもの全て

: でURLの部分文字列がパラメータにマッピングされる。

app.get('/users/:userId/books/:bookId', (req, res) => {
  console.log(req.params) // /users/1/books/23:title] => { userId: '1', bookId: '23' }
})

-, . はその文字のまま取り扱われる。なので、以下定義ではそれぞれ from, to および genus, species がキャプチャ対象。

/flights/:from-:to
/plantae/:genus.:species

HANDLER は複数指定できる。next() を呼ばないと次のハンドラには行かない。

app.get('/example/b', (req, res, next) => {
  console.log('the response will be sent by the next function ...')
  next()
}, (req, res) => {
  res.send('Hello from B!')
})

Arrayでもよい。

app.get('/example/c', [cb0, cb1, cb2])

express.Router というやつを使うと、いくつかの route や middleware 利用をまとめてモジュールとできる。

var express = require('express')
var router = express.Router()

// middleware that is specific to this router
router.use(function timeLog (req, res, next) {
  console.log('Time: ', Date.now())
  next()
})
// define the home page route
router.get('/', function (req, res) {
  res.send('Birds home page')
})
// define the about route
router.get('/about', function (req, res) {
  res.send('About birds')
})

module.exports = router
var birds = require('./birds')
app.use('/birds', birds)

middleware と route の実行順

定義した順。next() を呼ぶ限り、次の middleware or route が実行される。

next 仮引数

次の middleware 関数ってことらしい。

next('route') とすると、次の middleware へ飛ぶ(stack を辿らない)。以下は 1-12 が出力される。

app.get('/', [
  (req, res, next) => {
    console.log('1-1')
    next('route')
  },
  (req, res) => {
    console.log('1-2')
  }
])
app.get('/', (req, res) => {
  console.log('2')
})

テンプレートエンジン

テンプレートエンジンは I/F が準拠してるものは全て使える。

  • app.set('views', './views') でテンプレートファイルがあるディレクトリを指定する
  • app.set('view engine', 'pug') でテンプレートエンジンを指定する

上記が設定されていれば、 res.render() を呼び出しすればよい。

res.render('index', { title: 'Hey', message: 'Hello there!' })

エラーハンドラする middleware を複数

app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)

logErrors では next(err) として、次のエラーハンドラに err オブジェクトを渡してあげる必要がある。つまり next() に引数を渡すと次のエラーハンドラが実行されるってことらしい。

function logErrors (err, req, res, next) {
  console.error(err.stack)
  next(err)
}

デバッグ

環境変数で出力有無を制御できる。

$ DEBUG=express:* node index.js

値の指定方法は debug モジュールってやつを利用してて、名前空間的なアプローチっぽい。

app.set()

設定値的なやつを設定すると middleware をまたいで値の共有ができる。

freeCodeCampのサーバ起動スクリプトを追いかける

freeCodeCamp

アプリケーションサーバ起動方法がいくつかあるように見受けられるので、まずはそこらへんの整理から。

追いかけたブランチ

master 50a388d

gulp

ドキュメントに記載ある実行方法gulpfileの中を覗いてみたところnodemon使ってたりしてるのでローカル開発用かと。エントリーポイントはserver/server.js なのを確認

npm start

実態は babel-node server/server.js

npm start-production

名前的に production 実行用だろう。実態は node pm2Startpm2Start.jsが動いて、server/production-start.jsがキックされる。

server/production-start.jsではDB接続を確認してからserver/server.jsを動かしているってことみたい。

server/server.js

というわけで、こいつがエントリーポイント。

利用されてるライブラリとかフレームワーク

以下らへんを別途調べていく。

loopback がメインな Web-App フレームワークっぽい。ルーティングとかどうやって解決されるか謎いので、ちょっとそっちを調べよう。

freeCodeCampのmasterブランチをローカルで動かす

freeCodeCamp

こいつをカスタマイズしてローカルで動かしたくて弄っているログです。

master ブランチに docker-compose.yml が存在するので簡単にイケるんじゃね?と思ったのがスタートで、OSS Gate東京ミートアップ2017-02-20に参加した時にテーマとして選択してみた。ちなみにそのイベントでは動かせていない

暫定版の起動方法

ごにょごにょした結果、以下の手順を踏めば起動まで持っていけた。

ちなみに staging ブランチが本家で動いているものを差異があるようだったので master ブランチで動かしているが、 docker-compose.yml は staging ブランチにしかコミットされてないのでそっちから持ってきている。

git clone https://github.com/freeCodeCamp/freeCodeCamp.git
cd freeCodeCamp
git checkout -b master-20170222 50a388d

# `loopback` `debug` `normalizr` は固定しないとダメ
perl -pi -e 's/"loopback": "\^2.22.0"/"loopback": "~2.22.0"/' package.json
perl -pi -e 's/"debug": "\^2.2.0"/"debug": "~2.2.0"/' package.json
perl -pi -e 's/"normalizr": "\^2.0.0"/"normalizr": "~2.0.0"/' package.json

npm install
bower install
npm run build
cp sample.env .env
echo NODE_ENV=production >> .env

git checkout staging -- docker-compose.yml
docker-compose up

これで一旦 Web サーバーと MongoDB の 2コンテナが起動するので、別セッションで以下を実行すればチャレンジ(問題)がロードされる。

# データベースへロード
docker exec $(docker ps -f name=freecodecamp_server_1 -q) node seed

DBの内容は起動時に読み込まれるっぽいので、一度 docker-compose を終了させて再起動すれば反映される。

docker-compose up

カスタマイズしたいポイント

  • 言語切替 + 翻訳して日本語化
  • OAuth認証の向き先変更
  • ヘルプフォーラムの向き先変更

お前は何がしたいんだっていう話ですが、社内の研修リソースとしてうまいこと活用できないかなと目論んでいるわけです。本家にも貢献できればいいけれども、それは出来たらベースということで。

RSpec 3.2 has been released! されたので Notable Changes をメモ

これね。 http://rspec.info/blog/2015/02/rspec-3-2-has-been-released/

超絶抄訳です。

Windows CI

RSpec 3.1リリースした時にWindows上で動かなくしちまったらしい(3.1.xのパッチリリースでは直している)。なのでWindows環境でのCIを追加したぜ!という話。

Core: Pending Example Output Includes Failure Details

Pending exampleの出力にfailureメッセージ(?)を含むようにしたので、Pendingの詳細知りたい場合にわざわざコードを確認しなくてもおk。

Core: Each Example Now Has a Singleton Group

(うーん、日本語にうまく訳せないのだががが、)各Exampleからそれに対するExample groupの暗黙的な一部として扱うようにしたので、Example groupに対して適用されるメタデータがExampleにも適用されるとかなんとか。

Core: Performance Improvements

rspec-coreのオブジェクトアロケーションを30%ほど削減したのでパフォーマンスがアップ。

Core: New Sandboxing API

RSpec自身のテストのために利用されていたサンドボックス機能を公開APIにしたのでサードバーティなRSpec extensionsのテストでも利用できるぜっていう話。

Core: Shared Example Group Improvements

Shared example groupsのバグが直った!

  • Shared exampleでのFailuresで正しくバックトレースを出力するようにした
  • Shared exampleでのFailuresで出力されるre-runコマンドが正しくre-runされるようになった
  • Shared exampleへの行フィルタが正しく動くようになった

Expectations: Chain Shorthand For DSL-Defined Custom Matchers

カスタムマッチャを定義するDSLchainが短く書けるようになった。

Expectations: Output Matchers Can Handle Subprocesses

Outputマッチャでサブプロセスの出力も検証可能になった。

Expectations: DSL-Defined Custom Matchers Can Now Receive Blocks

カスタムマッチャを定義するDSLで作ったマッチャがブロックを受け取れるようになった。block_argメソッドな。

Mocks: any_args Works as an Arg Splat

any_argsマッチャが賢くなって、部分的な引数リストに適用出来るようになった。

Mocks: Mismatched Args Are Now Diffed

モックの引数マッチャでdiffがpretty-printされるようになった。これは見やすい。

Mocks: Verifying Doubles Can Be Named

RSpec 3.0と3.1で追加したテストダブルタイプにオプション名をサポートするのを忘れてて(テヘペロ)、3.2でサポートされたぜ。という話。

Rails: Instance Doubles Support Dynamic Column Methods Defined by ActiveRecord

ActiveRecordベースのinstance_doubleではテーブルカラム名メソッドははじめてコールされるタイミングで動的生成されるので、動的生成されるまでmethod_defined?が意図した通りに機能せず分かりづらい。なのでinstance_double生成時点で解決されるようにした。

Rails: Support Ruby 2.2 with Rails 3.2 and 4.x

タイトルの通り :)

Rails: New Generator for ActionMailer Previews

Rails 4.1でリリースされたActionMailer previewsがデフォルトtestディレクトリに出力されて、RSpecspecディレクトリを利用するのでうまく統合できてなかったので、RSpec 3.2でspecディレクトリに生成されるように統合したぜ。