Vue.jsでの簡単なログイン画面

Vue.js でのログイン画面をつくり、そこで入力された Email と Password が認証されるとトップページに遷移する、というものです。
トップページではログアウトボタンを押すとログイン画面に戻ります。

![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493435/6484c20a-4f55-47d6-e0e9-eeb1422c5f61.png)
![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493435/4659dd21-d2fc-94f2-797b-69f165226720.png)

https://github.com/yatsurugi55/vue-login

# はじめに
Vue 3 を使って認証しないと入れないページを作ります。
ログイン画面で入力されたユーザー情報は、APIサーバーに送られ、認証が成功するとトークンが返ってきます。
返ってきたトークンは Vuex で保持され、APIサーバーは Express で動かしています。

# ディレクトリツリー
```
src/
|--App.vue
|--api
|  |--auth.js // APIサーバーと接続
|--components
|  |--molecules
|  |  |--LoginForm.vue
|  |--organisms
|  |  |--LoginView.vue
|  |  |--TopView.vue
|--main.js
|--router
|  |--guards.js
|  |--index.js
|--store
|  |--index.js
|  |--modules
|  |  |--actions.js
|  |  |--mutation-types.js
|  |  |--mutations.js
```

# コンポーネント
### molecules/LoginForm.vue

- Email、Password のログインフォーム
- 上から渡されるログイン用のメソッドを実行


```src/components/molecules/LoginForm.vue
<template>
  <div class="login">
    <div class="form-item">
      <label for="email">Email</label>
      <input
        id="email"
        autocomplete="off"
        type="text"
        v-model="email"
      >
    </div>
    <div class="form-item">
      <label for="password">Password</label>
      <input
        id="password"
        autocomplete="off"
        type="password"
        v-model="password"
      >
    </div>
    <div class="form-item">
      <button class="button" @click="handle()">Button</button>
    </div>
  </div>
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'LoginForm',
  data () {
    return {
      email: '',
      password: ''
    }
  },
  props: {
    login: {
      type: Function,
      required: true
    }
  },
  methods: {
    handle () {
      return this.login({
        'user': {
          'email': this.email,
          'password': this.password,
        }
      })
      .catch(err => { throw err })
    }
  }
});
</script>

<style scoped>
.form-item {
  margin: 0 auto;
  text-align: center;
}

label {
  display: block;
}

input {
  width: 50%;
  padding: .5em;
  font: inherit;
}

button {
  padding: 0.5em;
  margin: 1em;
}
</style>
```

###organisms/LoginView.vue

- `login` アクションをディスパッチ
- 認証したらトップページへ遷移


```src/components/organisms/LoginView.vue
<template>
  <div class="login-view">
    <LoginForm :login="handleLogin" />
  </div>
</template>

<script>
import { defineComponent } from 'vue';
import LoginForm from '@/components/molecules/LoginForm.vue'

export default defineComponent({
  name: 'LoginView',
  components: {
    LoginForm
  },

  methods: {
    handleLogin (authInfo) {
      return this.$store.dispatch('login', authInfo)
        .then(() => {
          this.$router.push({ path: '/' })
        })
        .catch(err => { throw err })
    }
  }
});
</script>

<style scoped>
.login-view {
  width: 400px;
  margin: auto;
}
</style>
```

### organisms/TopView.vue

- 認証しないと入れないトップページ
- `logout`アクションをディスパッチしてログイン画面に遷移


```src/components/organisms/TopView.vue
<template>
  <h1>Top page</h1>
  <button class="button" @click="logout()">Logout</button>
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'TopView',
  methods: {
    logout () {
      return this.$store.dispatch('logout')
        .then(() => {
          this.$router.push('/login')
        })
        .catch(error => { throw error })
    }
  }
});
</script>
```

# vue-router

###index.js

- トップページとログインページの定義
- トップページには `meta`項目を加え、ログイン認証しないと入れないようにする

```src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Login from '../components/organisms/LoginView.vue'
import Top from '../components/organisms/TopView.vue'
import { authorizeToken } from './guards'

const routes = [
  {
    path: '/',
    name: 'Top',
    component: Top,
    meta: {
      requiresAuth: true
    }

  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})
router.beforeEach(authorizeToken)

export default router
```

###guards.js

- ページ遷移する条件で、index.js から呼び出す

```src/router/guards.js
import store from '../store'

export const authorizeToken = (to, from, next) => {
  if (to.matched.some(page => page.meta.requiresAuth) && (store.state.auth.token === null)) {
    next('/login')
  } else {
    next()
  }
}
```

# API実行モジュール

- ユーザー認証のための外部のAPIサーバーとやりとり
- アクションから呼び出すことになる

```src/api/auth.js
import axios from 'axios'

const headers = {}
headers['Content-type'] = 'application/json'

const config = {
  method: null,
  url: 'http://localhost:8081', // APIサーバー
  headers,
  data: null
}

export default {
  login: (authInfo) => {
    config.method = 'post'
    config.data = authInfo

    return axios.request(config)
      .then(res => res)
      .catch(error => { throw error })
  },
  logout: () => {
    config.method = 'delete'
    return axios.request(config)
      .then(res => res)
      .catch(error => { throw error })
  }
}
```

# Store

### index.js

- 認証情報を保持する
- トークンはローカルストレージに配置する

```src/store/index.js
import { createStore } from 'vuex'
import actions from '@/store/modules/actions'
import mutations from '@/store/modules/mutations'

const state = {
  auth: {
    token: localStorage.getItem('token'),
    userId: null
  }
}

export default createStore({
  state,
  actions,
  mutations,
})
```

###actions.js

- api/auth.js を呼び出して認証する
- 認証できたらトークンをローカルストレージに保管して、ミューテーションの処理を実行する
- ログアウトの際はローカルストレージからトークンを消す

```src/store/modules/actions.js
import auth from '@/api/auth'
import * as types from './mutation-types'

export default {
  login({ commit }, data) {
    return auth.login(data.user)
      .then((res) => {
        localStorage.setItem('token', res.data.token)
        commit(types.LOGIN, res.data)
      })
      .catch(error => { throw error })
  },
  logout({ commit }) {
    return auth.logout()
      .then(() => {
        localStorage.removeItem('token')
        commit(types.LOGOUT, { token: null, userId: null })
      })
      .catch(error => { throw error })
  }
}
```

###mutations.js
```src/store/modules/mutations.js
import * as types from './mutation-types'

export default {
  [types.LOGIN] (state, payload) {
    state.auth.token = payload.token
    state.auth.userId = payload.userId
  },
  [types.LOGOUT] (state, payload) {
    state.auth.token = payload.token
    state.auth.userId = payload.userId
  },
}
```

###mutation-types.js
```src/store/modules/mutation-types.js
export const LOGIN = 'LOGIN'
export const LOGOUT = 'LOGOUT'
```

#APIサーバー

api/auth.js からアクセスするAPIサーバーを簡易的につくります。
今回いずれも docker 上に構築しましたが、本体は8080ポート、こちらのAPIサーバーは8081ポートで Express で動かしています。

以下のコードを `node index.js` で立ち上げただけの簡単なものです。

```index.js
const express = require('express');
const cors = require('cors');
const app = express();

const users = {
  'hoge@hoge.com': {
    userId: 1,
    token: '1234567890abcdef'
  }
};

app.use(cors({
  origin: 'http://localhost:8080', // source url
  credentials: true,
  optionsSuccessStatus: 200
}))

app.post('/', (req, res) => {
  res.send(users['hoge@hoge.com']);
});

app.delete('/', (req, res) => {
  res.send('Deleted.')
});

app.listen(8081, () => console.log('Listening on port 8081'));
```