DockerをつかってVueとRailsの開発環境にユーザー認証機能を実装する

GWの記事3投目です。
# 一連の記事
1. [DockerをつかってVueとRailsの開発環境をつくる](https://qiita.com/harachan/items/4561d54948fe650cf611)
2. [DockerをつかってVueとRailsの開発環境にユーザー認証機能を実装する](https://qiita.com/harachan/items/24f7fbda7c41a4115bf5)この記事！！
3. [DockerをつかったVueとRailsの環境にAPIを実装する](https://qiita.com/harachan/items/ad9fce3f4b84f49d583a)

# 本記事の目的
[DockerをつかってVueとRailsの開発環境をつくる](https://qiita.com/harachan/items/4561d54948fe650cf611)の続編です。
前回環境を作ったところに、以下の機能を追加する
- ユーザーの新規登録
- ログイン機能
- Tokenの発行

# 手順
## サーバー側
### CORSの設定
cors設定に必要なものとついでに```has_secure_password```を使用するためのものをgemfileに記載

```gemfile:server/Gemfile
# Gemfile
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'#28行目あたりのこの行のコメントアウトを外す
gem 'bcrypt', '~> 3.1.7' #追記
```
外したら保存して```docker-compose run rails bundle install```を実行する

```rb:server/config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8080'#Dockerで立ててるWebコンテナ（Vue）のアドレス

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

### Userモデルの作成
Userモデルを作成します

```bash:bash
docker-compose run rails rails g model User email:string name:string password_digest:string token:string

# 終わったら
docker-compose run rails rails db:migrate
```

### 認証処理を実装する

```rb:server/app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate!

  private

  def authenticate!
    authenticate_or_request_with_http_token do |token, options|
      User.find_by(token: token).present?
    end
  end

  def current_user
    @current_user ||= User.find_by(token: request.headers['Authorization'].split[1])
  end
end
```

```before_action :authenticate!```を使うことで全コントローラーのアクションに認証を掛けられる
認証なしでもAPIを使いたい場合は```skip_before_action```を使う
###UsersControllerの実装

```bash:bash
docker-compose run rails rails g controller Users create sign_in
```

ルーティングの調整を以下のようにする

```rb:server/config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [ :create ] do
    collection do
      post 'sign_in'
    end
  end
end
```
### ログイン機能と新規登録機能を実装

```rb:
class UsersController < ApplicationController
  skip_before_action :authenticate!, only: [ :create, :sign_in ]
  
  # 新規登録 POST/user
  def create
    @user = User.new(email: params[:email], password: params[:password], name: params[:name])

    if @user.save
      render json: @user
    else
      render json: { errors: @user.errors.full_messages }, status: 400
    end
  end

  # ログイン POST/sign_in
  def sign_in
    @user = User.find_by(email: params[:email])

    if @user && @user.authenticate(params[:password])
      render json: @user
    else
      render json: { errors: ['ログインに失敗しました'] }, status: 401
    end
  end
end
```

これでサーバー側の準備は終了です。
## フロント側
### axiosの準備とElement-uiの導入
HTTP通信を行うためにAxiosを導入します

```bash:bash
docker-compose run web npm install --save axios vue-axios element-ui
```

終わったら、Vueで読み込むための設定をします。

```js:front/src/main.js
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'// 追記
import 'element-ui/lib/theme-chalk/index.css'// 追記
import axios from 'axios'// 追記
import VueAxios from 'vue-axios'// 追記

Vue.use(VueAxios, axios)// 追記
Vue.use(ElementUI)// 追記

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

```

### Headerの作成
アプリヘッダーを作成して表示させます。

```vue:front/src/components/appheadder.vue
<template>
  <div class="appheadder">
    <router-link to="/" class="title">プロジェクト名</router-link>
    <div class="menu">
      <ul id="nav">
        <span v-if="!$store.state.token">
          <li><router-link to="/login">ログイン</router-link></li>
          <li><router-link to="/signup">新規登録</router-link></li>
        </span>
        <span v-else>
          <li><router-link to="/login">検索</router-link></li>
          <li><a @click="dologout">ログアウト</a></li>
        </span>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'appheadder',
  methods: {
    dologout () {
      this.$store.dispatch('doRegistrationToken', null)
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.appheadder{
  background-color: rgb(92, 182, 153);
  height: 50px;
}
.menu{
  text-align: right;
  float: right;
  position:  fixed;
  right: 0;
  top: 0;
}
.title{
  font-size: 40px;
  text-align: center;
  margin: auto;
  text-decoration: none;
  color: rgb(0, 0, 0);
  font-weight: bold;
}
#nav {
  padding: 0 0 0 0;
  list-style: none;
  overflow: hidden;
  display: inline;
}

#nav li {
  width: 12vw;
  text-align: center;
  background-color: #333;
  float: left;
  height: 50px;
  line-height: 50px;
  margin-right: 2px;
}

#nav li a {
  text-decoration: none;
  color: #fff;
  font-weight: bold;
}

</style>
```

```vue:front/src/App.vue
<template>
  <div id="app">
    <Appheadder/>
    <router-view/>
  </div>
</template>

<script>
import Appheadder from './components/appheadder.vue'
export default {
  name: 'Home',
  components: {
    Appheadder
  }
}
</script>

<style>
*{
margin: 0;
}
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>
```

```vue:front/src/views/home.vue
<template>
  <div class="home">
  </div>
</template>

<script>

export default {
  name: 'Home',
  components: {
  }
}
</script>
```

これを変更してページの状態を確認しましょう。
コンテナを立ち上げていなければ立ち上げてhttp://localhost:8080 にアクセスして、以下のようなページが表示されていればOKです。
![FireShot Capture 007 - mybutler - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323051/e30374a2-4bd1-9677-4a8a-c642dcd14cdb.png)

### ログインページと新規登録画面の作成
まず新規登録画面を作成します

```vue:front/src/views/signup.vue
<template>
  <div class="signup">
    <h1>新規登録</h1>
    <div class="loginForm">
      <table>
        <tr>
          <th>ID</th>
          <td><el-input placeholder="Please input" v-model="id"></el-input></td>
        </tr>
        <tr>
          <th>パスワード</th>
          <td><el-input placeholder="Please input password" v-model="pass" show-password></el-input></td>
        </tr>
      </table>
      <el-button type="primary" @click="dologin">新規登録</el-button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'signup',
  data () {
    return {
      id: '',
      pass: ''
    }
  },
  components: {
  },
  methods: {
    dologin () {
      this.axios.post('http://0.0.0.0:3000/users/', {
        name: this.id,
        pwd: this.pass
      },
      {
        headers: {
          'Content-Type': 'application/json'
        }
      })
        .then((response) => {
          this.$store.dispatch('doRegistrationToken', response.data.token)
        })
        .catch((e) => {
          console.log(e)
        })
    }
  }
}
</script>
<style scoped>
  .loginForm{
    width: 80%;
    padding: 10vh 5vw;
    margin: auto;
    background-size: cover;
  }
  .loginForm *{
    margin: auto;
  }
</style>

```

次にログイン画面も作りますがほとんど一緒です。

```vue:front/src/views/login.vue
<template>
  <div class="login">
    <h1>ログイン</h1>
    <div class="loginForm">
      <table>
        <tr>
          <th>ID</th>
          <td><el-input placeholder="Please input" v-model="id"></el-input></td>
        </tr>
        <tr>
          <th>パスワード</th>
          <td><el-input placeholder="Please input password" v-model="pass" show-password></el-input></td>
        </tr>
      </table>
      <el-button type="primary" @click="dologin">ログイン</el-button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Login',
  data () {
    return {
      id: '',
      pass: ''
    }
  },
  components: {
  },
  methods: {
    dologin () {
      this.axios.post('http://0.0.0.0:3000/users/sign_in', {
        name: this.id,
        pwd: this.pass
      },
      {
        headers: {
          'Content-Type': 'application/json'
        }
      })
        .then((response) => {
          this.$store.dispatch('doRegistrationToken', response.data)
        })
        .catch((e) => {
          console.log(e)
        })
    }
  }
}
</script>
<style scoped>
  .loginForm{
    width: 80%;
    padding: 10vh 5vw;
    margin: auto;
    background-size: cover;
  }
  .loginForm *{
    margin: auto;
  }
</style>
```

続いてルーティングを直していきます。
まずヘッダーを直します

```vue:front/src/components/appheadder.vue
<template>
  <div class="appheadder">
    <router-link to="/" class="title">プロジェクト名</router-link>
    <div class="menu">
      <ul id="nav">
        <span v-if="!$store.state.token">
          <li><router-link to="/login">ログイン</router-link></li><!--変更-->
          <li><router-link to="/signup">新規登録</router-link></li><!--変更-->
        </span>
        <span v-else>
          <li><router-link to="/login">検索</router-link></li><!--変更-->
<!--以下略-->
```

```js:front/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/login.vue'
import Signup from '../views/signup.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/login',
    name: 'login',
    component: Login
  },
  {
    path: '/signup',
    name: 'signup',
    component: Signup
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
```

### ストアにトークンを保存するための準備

```js:front/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: ''
  },
  mutations: {
    RegistrationToken (state, payload) {
      state.token = payload.token
    }
  },
  actions: {
    doRegistrationToken ({ commit }, token) {
      commit('RegistrationToken', { token })
    }
  },
  modules: {
  }
})
```

これで確認してみましょう！
![FireShot Capture 008 - mybutler - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323051/6e9945f1-535a-8d05-7e20-28225e8a1458.png)
ここでユーザーを登録すると右上のMENUが検索とログアウトになれば成功です。
正常にユーザーが新規登録でき、Tokenが帰ってきています。
ログアウトを押してもとに戻るのも確認してください。
その後ログインから、先程登録した情報でログインできるか確認してみてください。

# あとがき
今後も続いて開発していけたらこうしてまとめていこうと思いますのでよろしくおねがいします。
あ〜、GWもあと1日か〜

# 参考にさせていただいた記事
めちゃくちゃおせわになりました！！
ありがとうございます！
- [Rails5 APIで認証付きのWebAPIを作ってみる](https://qiita.com/ochiochi/items/966b884eb17045dfb929#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB)
- [Rails5 APIモードでつくるかんたんなトークンベース認証](https://dev.m6a.jp/entry/2018/11/14/162259)
- [vue-cliでaxiosを使用する(設定から使用方法まで)](https://qiita.com/right1121/items/092ac7ff747e1c47b2b1)