←Nuxt.jsで認証認可入り掲示板APIのフロントエンド部分を構築する #3 個別記事ページの作成
サインアップ(登録)ページの作成
サインアップページ及びそのロジックを作っていきますが、難易度が一気に上がります。
一気にすべて理解しようとせず、少しずつコードを読み解いていってください。
まずはサインアップページを作ります。
<template>
<v-app id="inspire">
<v-main>
<v-container
class="fill-height"
fluid
>
<v-row
align="center"
justify="center"
>
<v-col
cols="12"
sm="8"
md="4"
>
<form @submit.prevent="signUp">
<v-card class="elevation-12">
<v-toolbar
color="primary"
dark
flat
>
<v-toolbar-title>SignUp form</v-toolbar-title>
</v-toolbar>
<v-card-text>
<ul v-if="errors">
<li v-for="(message, i) in errors.full_messages" :key="i">
{{ message }}
</li>
</ul>
<v-text-field
id="name"
v-model="name"
label="name"
name="name"
prepend-icon="mdi-account"
type="text"
/>
<v-text-field
id="email"
v-model="email"
label="email"
name="email"
prepend-icon="mdi-email"
type="email"
/>
<v-text-field
id="password"
v-model="password"
label="Password"
name="password"
prepend-icon="mdi-lock"
type="password"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" type="submit">
登録
</v-btn>
</v-card-actions>
</v-card>
</form>
</v-col>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
data () {
return {
name: '',
email: '',
password: '',
errors: []
}
},
methods: {
async signUp () {
try {
const data = await this.$store.dispatch('signUp', {
name: this.name,
email: this.email,
password: this.password
})
this.$store.commit('setUser', data.data)
this.$router.push('/')
} catch (e) {
this.errors = e.data.errors
}
}
}
}
</script>
フォームを送信するとsignUp()
メソッドが発火し、storeのsignUpアクション(この後作ります)でRails APIにPOSTし、正常終了したらsetUser
でユーザーデータをstoreに保存(この後作ります)。
エラーが起きたらcatchしてエラーを出力している、という形です。
参考:Vue Material Component Framework — Vuetify.js
store側の実装
ログインに関わる処理はサイト全体で共通して使いそうなので、store/index.jsに書いていきます。
export const state = () => ({
user: null,
auth: {},
logged_in: false
})
export const getters = {
user (state) {
return state.user
},
logged_in (state) {
return state.logged_in
},
auth (state) {
return state.auth
}
}
export const mutations = {
setUser (state, value) {
state.logged_in = !!value
state.user = value
},
setAuth (state, value) {
state.auth = value
}
}
export const actions = {
async signUp (_c, user) {
return await this.$axios.$post('/v1/auth', user)
}
}
ログインしているかどうかの判定に使うlogged_inに注目。
setUser(state, value) { state.logged_in = !!value state.user = value },
ユーザー情報セットの際、二重否定を使うことでuserの中身があればtrue, なければfalseが入るようにしています。
axiosの拡張
続いてaxiosのデフォルト挙動を変えます。
axiosでサーバサイドと通信する度にgetter['auth']
してヘッダに付与するのは冗長かつしんどいので共通化します。
また、レスポンスが返ってくるたびにアクセストークンをstoreに保存するのも毎度書いていたら辛いので一緒に共通化します。
export default ({ $axios, store }) => {
$axios.onRequest((config) => {
config.headers = store.getters.auth
})
$axios.onResponse((response) => {
if (response.headers['access-token']) {
const authHeaders = {
'access-token': response.headers['access-token'],
client: response.headers.client,
expiry: response.headers.expiry,
uid: response.headers.uid
}
store.commit('setAuth', authHeaders)
}
})
$axios.onError((error) => {
return Promise.reject(error.response)
})
}
plugins: [
+ '~/plugins/axios'
],
これで、以後axiosを使う度、リクエストにはアクセストークンが自動付与されて、レスポンス後はトークンがstoreに自動保存されます。
セッション永続化の準備
今のままだと認証情報はstoreに入れているだけなので、画面遷移する分には問題ないのですが、F5押下のようにリロードするとログアウト状態になってしまいます。
これを永続化するためにはトークン情報をcookieかWebStorageに保存する必要があります。
今回は簡易的な機能のためlocalStorageを使いますが、トークンをlocalStorageに保存することはセキュリティ的には弱いため、ちゃんとしたプロダクトでは避けた方が良いかもしれません。
参考:HTML5のLocal Storageを使ってはいけない(翻訳)
- cookie:サーバサイド・クライアントサイドどちらでも読み込み書き込みできるため、SSRでも使える。ただし値セットが面倒
- WebStorage(LocalStorage):クライアントサイドのみのため、SSRでは使えない。そのため一瞬未ログイン画面が出てしまうデメリットがあるが、pluginを入れれば超簡単にstoreを永続化できる
今回は手軽さを取ってLocalStorageを使います。
$ yarn add vuex-persistedstate
plugins: [
- '~/plugins/axios'
+ '~/plugins/axios',
+ { src: '~/plugins/localStorage.js', ssr: false }
],
import createPersistedState from 'vuex-persistedstate'
export default ({ store }) => {
createPersistedState({
key: 'bbs_session',
paths: [
'auth'
]
})(store)
}
これだけの設定で、state.auth
がbbs_session
というlocalStorageのkeyで保存されます。
特にlocalStorageを意識することなくstate.auth
に値をセットするとlocalStorageにもセットされるというすぐれものです。
とはいえこれだけでは保存しているだけで、画面リロード時に読み込む処理が入っていません。
そのロジックは共通レイアウトに入れてしまいます。
共通レイアウトの修正
ここまでで機能の大部分はできました。
ですが共通レイアウトが暗く見づらい上、不要なサイドバー等があるので作り直す勢いで修正します。
また、前述の通り画面リロード時のセッション復元も作ります。
<template>
<v-app>
<v-app-bar
fixed
app
>
<n-link to="/">
<v-toolbar-title v-text="title" />
</n-link>
<v-spacer />
<span v-if="logged_in">
{{ current_user.name }}さん
</span>
<v-btn
v-if="logged_in"
icon
>
<v-icon>mdi-logout</v-icon>
</v-btn>
<v-btn
v-else
icon
to="/sign_up"
nuxt
>
<v-icon>mdi-account-plus</v-icon>
</v-btn>
</v-app-bar>
<v-main>
<v-container>
<nuxt />
</v-container>
</v-main>
<v-footer
app
>
<span>© {{ new Date().getFullYear() }}</span>
</v-footer>
</v-app>
</template>
<script>
export default {
data () {
return {
title: 'Sample bbs'
}
},
computed: {
current_user () {
return this.$store.getters.user
},
logged_in () {
return this.$store.getters.logged_in
}
},
async beforeMount () {
const session = JSON.parse(window.localStorage.getItem('bbs_session'))
if (session && Object.keys(session.auth).length) {
await this.$axios.$get('/v1/auth/validate_token')
.then(data => this.$store.commit('setUser', data.data))
.catch(() => {
this.$store.commit('setUser', null)
this.$store.commit('setAuth', {})
})
}
}
}
</script>
beforeMount()
はCSR(クライアントサイドレンダリング)でのみ最初に実行されます。
localStorageはサーバサイドで使えないので、クライアントサイド描画されるタイミングでセッションの値を取得し、auth
が保存されている場合(画面更新直前までログイン状態だった場合)にvalidate_tokenを叩きにいき、user情報とauthの更新をしてログイン状態を復帰します。
無効なauthパラメータだった場合はuserとauthをクリアします(自動的にlocalStorageもクリアになります)。
ここまで組み込めば、ログイン後に画面更新してもセッション復帰する状態になるはずです。
ページを開いた直後は未ログイン状態ですが、ちょっと待てばログインになります。
このタイムラグが気になる場合はcookieで実装が必要になります。
vuetify: {
customVariables: ['~/assets/variables.scss'],
theme: {
- dark: true,
+ dark: false,
themes: {
- dark: {
+ light: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
そして色変え。これでだいぶスッキリするはずです。
参考
Rails devise token authとNuxt.jsを連携(Twitter認証)
Nuxt.jsのStoreによるデータ保存 [vuex-persistedstate][js-cookie]
続き
→Nuxt.jsで認証認可入り掲示板APIのフロントエンド部分を構築する #5 ログイン・ログアウトの実装
【連載目次へ】