react-router でアクセス制御

# TL;DR
- https://gist.github.com/gin0606/f2433fa1dd1ff9464fe2ac064dddd87b
- 細かい表示の出し分けはこちら↓
  - [React でユーザーの権限や役割によって Component を見せたり見せなかったりする - Qiita](http://qiita.com/gin0606/items/03de2353bb32b92a6109)

# やりたいこと
ユーザーの権限やロールなどによる、 react-router で定義した route へのアクセス制御

# やらないこと
使用しているライブラリなどの解説

# 前提
- ES2015
- reactjs v15.x.x
- react-router v2.8.x
- flux

---

ユーザーの属性はアプリケーションの仕様によるので、今回はユーザーの「強さ」によってアクセスの可不可を決める。

各自、開発するアプリケーションで使用する言葉に置き換えて下さい。

# ユーザーの強さ（`Strength`）一覧
- yowai: 初級者
- hutuu: 中級者
- tsuyoi: 上級者
- saikyou: 最強

# Route 一覧
- `/`
- `/doujo`
- `/doujo/yowai`
- `/doujo/hutuu`
- `/doujo/tsuyoi`
- `/doujo/saikyou`
- `/doujo/yowai/promotion`
- `/doujo/hutuu/promotion`
- `/doujo/tsuyoi/promotion`

## 各 Route に入れる人
- `/`
  - 世界
  - 大体 `App` みたいなのを指定してると思うので、今回もそうします。
- `/doujo`
  - 道場の入り口
  - 誰でも入れる
- `/doujo/:tsuyosa`
  - 自分が属する強さと、1つ下の道場に入ることが出来る
  - 例：中級者なら hutuu と yowai 
- `/doujo/:tsuyosa/promotion`
  - 昇級試験
  - 自分が属する強さの試験しか受けることが出来ない

# アクセス制御前のコード
hutuu 以上はだいたい同じなので省略👷

```js
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, browserHistory } from 'react-router';

class App extends React.Component {
  static propTypes = {
    children: React.PropTypes.node,
  };

  render() {
    return this.props.children;
  }
}

class Doujo extends React.Component {
  static propTypes = {
    children: React.PropTypes.node,
  };
  render() {
    return (<div>
      <h1>道場</h1>
      {this.props.children}
    </div>);
  }
}

class YowaiDoujo extends React.Component {
  static propTypes = {
    children: React.PropTypes.node,
  };
  render() {
    return (<div>
      <h2>初級者向け道場</h2>
      {this.props.children}
    </div>);
  }
}

class YowaiPromotionTest extends React.Component {
  static propTypes = {
    children: React.PropTypes.node,
  };
  render() {
    return (<div>
      <h3>初級者昇級試験</h3>
      {this.props.children}
    </div>);
  }
}

const routes = (
  <Route path="/" component={App}>
    <Route path="/doujo" component={Doujo}>
      <Route path="yowai" component={YowaiDoujo}>
        <Route path="promotion" component={YowaiPromotionTest} />
      </Route>
    </Route>
  </Route>
);

const router = (
  <Router history={browserHistory}>
    {routes}
  </Router>
);

ReactDOM.render(
  router,
  document.getElementById('app')
);
```

# アクセス制御を実装する
ユーザーの強さはサーバーに問い合わせて取得するということにする。

## Route の設定
以下のように Route に `tsuyosa` を指定していく

```js
const routes = (
  <Route path="/" component={App}>
    <Route path="/doujo" component={Doujo}>
      <Route path="yowai" component={YowaiDoujo} tsuyosa={['yowai', 'hutuu']}>
        <Route path="promotion" component={YowaiPromotionText} tsuyosa={['yowai']} />
      </Route>
    </Route>
  </Route>
);
```

## アクセス制御
アクセス制御の実装は *Higher Order Component* (*Inheritance Inversion*) パターン（？）を使って実装する。

[ReactのHigher Order Components詳解 : 実装の2つのパターンと、親Componentとの比較 | プログラミング | POSTD](http://postd.cc/react-higher-order-components-in-depth/)

HOC については↑これを読んで下さい。

何故 HOC を使うかについては端折るけど、結果として以下2つの良い点があった。

- アプリケーションの大本の Component からアクセス制御の実装を完全に切り離せる
- ルーティングの設定のところに `withAuthorization` と `tsuyosa` を書けるので、アクセス制御の設定などを一箇所にまとめられる

HOC の用法として適切なのかについては、各自で判断して下さい。

### 実装
- Flux の「 Action -> Dispatcher -> Store -> ChangeEvent 」の流れを `setTimeout` で代用する
- 起動時に constructor が呼ばれるけど、サーバーとの通信前なので Route の情報を取っておいて後で処理する
  - `startupRoute`
  - 使い終わったら `null` にする
- routes から設定した `tsuyosa` を取得するのに getTuyosa という関数を実装したけど、実装がデカイので gist に上げた
  - https://gist.github.com/gin0606/f2433fa1dd1ff9464fe2ac064dddd87b#file-react-router-util-js

```js
function withAuthorization(WrappedComponent) {
  return class AuthorizationComponent extends WrappedComponent {
    constructor(...args) {
      super(...args);

      this.state = {
        // 初期化処理が終わったか判定するために userStrength の初期値は undefined にする
        userStrength: undefined,
        isAuthorized: true,
        // 起動時は userStrength が不明なので、あとで route にアクセス可能か判定する
        startupRoute: {
          routes: this.props.routes,
          location: this.props.location,
        },
      };

      // 今回はサーバー通信出来ないので、 `setTimeout` で代用する
      // 実際は Store からの Change Event を受け取って、`setState` を呼ぶ
      setTimeout(() => {
        this.setState({
          userStrength: 'tsuyoi',
        });
      }, 1000);
    }

    // <Link> タグでの移動時の制御
    componentWillReceiveProps(nextProps) {
      if (super.componentWillReceiveProps) { super.componentWillReceiveProps(nextProps); }

      const { routes, location } = nextProps;
      this.setState({
        isAuthorized: this.isAuthorized(routes, location, this.state.userStrength),
      });
    }

    // 起動時の制御
    componentWillUpdate(nextProps, nextState) {
      if (super.componentWillUpdate) { super.componentWillUpdate(nextProps, nextState); }

      if (!nextState.startupRoute) { return; }
      if (this.state.userStrength === undefined) { return; }

      const { routes, location } = nextState.startupRoute;
      nextState.isAuthorized = this.isAuthorized(routes, location, this.state.userStrength);
      nextState.startupRoute = null;
    }

    isAuthorized(routes, location, strength) {
      const tsuyosa = getTuyosa(routes, location.pathname);
      if (!tsuyosa) { return true; }
      return tsuyosa.includes(strength);
    }

    render() {
      if (this.state.userStrength === undefined) { return <h1>準備中</h1>; }
      if (!this.state.isAuthorized) { return <h1>君は入れない</h1>; }
      return super.render();
    }
  };
}
```

### Route の設定で使う
以下のように `withAuthorization` を使う

```js
const routes = (
  <Route path="/" component={withAuthorization(App)}>
    <Route path="/doujo" component={Doujo}>
      <Route path="yowai" component={YowaiDoujo} tsuyosa={['yowai', 'hutuu']}>
        <Route path="promotion" component={YowaiPromotionText} tsuyosa={['yowai']} />
      </Route>
    </Route>
  </Route>
);
```

これで、 tsuyoi 人が */doujo/yowai/promotion* にアクセスしようとしたら「君は入れない」と表示されるようになる。

`getTuyosa` は子 Route に `tsuyosa` を指定してなかったら、親の `tsuyosa` を取りに行くようになってるのだけど、強さとか道場とかでいい例思いつかなくて書いてない。

おしまい。