5
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-09-21

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

image4.png
image5.png

はじめに

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'));
5
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?