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