この記事は、Jamstack Advent Calendar 11日目の記事になります。
今回は、NuxtJS で、Firebase Authentication を利用した時に最もよく使用する実装方法についてまとめました。
なお、今回実装したサンプルは 以下の GitHub リポジトリに保存しています。
環境構築
アプリの環境構築は次の通りに行いました。
npx create-nuxt-app firebase-auth-sample
今回は次のような状態で設定しました。
- Project name : TypeScript
- Package manager: npm
- UI framework: None
- Nuxt.js modules: Axios, Progresive Web App, Content (今回は不要)
- Linting modules: ESLint, Prettier, Lint Staged files, Stylelint
- Testing Framework: Jest
- Rendering mode: Single Page App
- Developmenyt target: Static
- Development tools: jsconfig,.json
- Continuous integration: Github Actions
今回は、Vue composition API を利用して、Firebase を使用しての認証回りについて実装します。作成後に、このディレクトリにして、
npm i --save firebase @vue/composition-api
を実行してライブラリをインストールします。
Vue compositino API での設定は、/plugins/composition.ts
にて、
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)
として、nuxt.config.ts
にて
export.default{
...
plugins: ['~/plugins/composition'],
}
を追加してください。
Firebase Authentication について
Firebase Authentication は、Firebase のライブラリの一つとして提供される認証ライブラリです。様々な認証ライブラリと異なり、Basic 認証だけでなく、Google やTwitter などのOauth 認証やSMS, メールでの2段階認証も用意されています。このライブラリは、Node.js だけでなく、Python やGolang, Ruby などの他言語も利用できます。
この記事では、NuxtJS で Firebase Authentication で個人的によく利用するテンプレートについて書きました。
それまでに至るまでの初期設定の部分は今回は省略して、実装のために設定したコードを掲載します。
import Firebase from "firebase/app";
import "firebase/auth";
// config について一部省略。
const config = {
apiKey: "xxxxxxx",
authDomain: "xxxxxxxxxxx",
databaseURL: "xxxxxxxxx",
projectId: "xxxxxxxxxxxxxxxxxxx",
storageBucket: "xxxxxxxxxxxxxxxx",
messagingSenderId: "xxxxxxxxxxxx",
appId: "xxxxxxxxxxxxxxxxxxxxx",
measurementId: "xxxxxxxxxxxxxxxx"
}
const firebase = Firebase.apps.length? Firebase.app():Firebase.initializeApp(config);
export const auth = firebase.auth();
Provide, Inject について
今回、グローバルステートの管理にはProvide, Inject を使用しました。それらの特徴や実装の仕方については次の記事を参考にしてください。
Vue Composition API でprovide , inject を用いたデータの状態を管理する方法 - Qiita
このState を扱ったコードは次の通りです。
import {provide,inject, InjectionKey, reactive, toRefs} from "@vue/composition-api";
export type UserType = {
id?:string,
name:string,
email:string,
thumbnail:string,
}
export type ErrorType = {
state:number
message:string
}
export type GlobalStateType = {
user:UserType,
error?:ErrorType
}
export const useGlobalState = ()=>{
const globalState =reactive<GlobalStateType>({
user:{
id:"",
name:"",
email:"",
thumbnail:""
}
});
const setUserState = (state: UserType)=>{
globalState.user.id=state.id;
globalState.user.email=state.email;
globalState.user.thumbnail=state.thumbnail;
globalState.user.name=state.name;
}
const cleanUserState = () =>{
globalState.user={
id:"",
name:"",
email:"",
thumbnail:""
}
}
return {
...toRefs(globalState),
setUserState,
cleanUserState
}
}
type StateType = ReturnType<typeof useGlobalState>
export const GlobalStateKey:InjectionKey<StateType> =Symbol("GlobalState");
export const ErrorStateKey: InjectionKey<ErrorType> = Symbol("ErrorState");
export const provideGlobalState= ()=>{
provide(GlobalStateKey,useGlobalState());
};
export const injectGlobalState =()=>{
const state = inject(GlobalStateKey);
if(!state){
throw Error("Unable to install User State");
}
return state;
}
Firebase Authentication 周りで使用したコードについて
今回 Firebase 周りで使用したコードについては次の通りです。
import {auth} from "@/plugins/firebase.config";
import Firebase from "firebase";
type SuccessResType ={
status: "ok",
data: Firebase.auth.UserCredential,
}
type FailureResType ={
status: "failed",
message: string,
}
export const SigninWithBasic = async (email:string, password:string):Promise<SuccessResType|FailureResType>=>{
try {
const loginUser = await auth.signInWithEmailAndPassword(email,password);
return {
status:"ok",
data: loginUser
}
} catch(e){
console.error(e);
return {
status:"failed",
message:e.message
}
}
}
export const SignUpWithBasic = async (email:string, password: string):Promise<SuccessResType|FailureResType>=>{
try {
console.log(email,password)
const createdUser = await auth.createUserWithEmailAndPassword(email,password);
console.log("作成された情報について",createdUser);
return {
status:"ok",
data : createdUser
}
} catch(e){
console.error(e);
return {
status: 'failed',
message: e.message
}
}
}
export const SignOut = async ():Promise<void|FailureResType> =>{
try {
await auth.signOut();
} catch(e){
console.error(e);
return {
status:"failed",
message:e.message
}
}
}
新規登録のページ
URL は、http://localhost:3000/signup です。
デザインは次のようになっています。
コードは次の通りです、
<template>
<div class="container">
<div class="card">
<h3>メール</h3>
<p class="my-input">
<input v-model="email" placeholder="" type="email" />
</p>
<h3>パスワード</h3>
<p class="my-input">
<input v-model="password" type="password" />
</p>
<h3>パスワード(確認用)</h3>
<p class="my-input">
<input v-model="passwordVerify" type="password" />
</p>
<nuxt-link to="/signin">ログインページに戻る</nuxt-link>
<p class="my-input">
<button @click="signUpBasic()">アカウント登録</button>
</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api'
import { SignUpWithBasic } from '~/utils/firebase/auth'
import { injectGlobalState } from '~/utils/states/user'
export default defineComponent({
name: 'SignUpPage',
setup(props: any, { root }) {
// TODO: この部分を reactive で実装したほうがいいかもしれない
const email = ref('')
const password = ref('')
const passwordVerify = ref('')
const userState = injectGlobalState()
const signUpBasic = () => {
if (password.value !== passwordVerify.value) {
alert('パスワードが違います。')
} else {
const emailValue = email.value
const passwordValue = password.value
SignUpWithBasic(emailValue, passwordValue)
.then((createdUser) => {
console.log(createdUser)
if (createdUser.status === 'ok') {
const userInfo = createdUser.data.user
userState.setUserState({
id: userInfo ? userInfo.uid : '',
email: userInfo && userInfo.email ? userInfo.email : '',
name:
userInfo && userInfo.displayName ? userInfo.displayName : '',
thumbnail:
userInfo && userInfo.photoURL ? userInfo.photoURL : '',
})
root.$router.push('/');
} else {
alert('アカウント登録失敗')
}
})
.catch(() => {
alert('エラーが発生しました')
})
}
}
return {
email,
password,
passwordVerify,
signUpBasic,
}
},
})
</script>
<style>
.container {
margin: 0 auto;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.title {
font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: block;
font-weight: 300;
font-size: 100px;
color: #35495e;
letter-spacing: 1px;
}
.subtitle {
font-weight: 300;
font-size: 42px;
color: #526488;
word-spacing: 5px;
padding-bottom: 15px;
}
.links {
padding-top: 15px;
}
</style>
ログインページの実装
URL は、http://localhost:3000/signin です。
画面は次のように表示されていると思います。
コードは次の通りです。
<template>
<div class="container">
<div class="card">
<h3>メール</h3>
<p class="my-input">
<input v-model="email" placeholder="" type="email" />
</p>
<h3>パスワード</h3>
<p class="my-input">
<input v-model="password" type="password" />
</p>
<nuxt-link to="/signup">アカウントの登録はこちら</nuxt-link>
<p class="my-input">
<button @click="loginBasic()">ログイン</button>
</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api'
import { SigninWithBasic } from '@/utils/firebase/auth'
import { injectGlobalState } from '~/utils/states/user'
export default defineComponent({
name: 'SigninPage',
setup(props: any, { root }) {
// local States
const email = ref('')
const password = ref('')
// Global States
const userState = injectGlobalState()
console.log(userState.user)
// method Functions
const loginBasic = async () => {
try {
const emailValue = email.value
const passwordValue = password.value
const currentUser = await SigninWithBasic(emailValue, passwordValue)
if (currentUser.status === 'ok') {
const userInfo = currentUser.data.user
console.log(userState)
userState.setUserState({
id: userInfo ? userInfo.uid : '',
email: userInfo && userInfo.email ? userInfo.email : '',
name: userInfo && userInfo.displayName ? userInfo.displayName : '',
thumbnail: userInfo && userInfo.photoURL ? userInfo.photoURL : '',
})
} else {
alert('ログイン失敗')
}
root.$router.push('/')
} catch (e) {
alert(e)
}
}
return {
email,
password,
loginBasic,
}
},
})
</script>
<style>
.container {
margin: 0 auto;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.my-input {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
</style>
ログインしているかどうかを知る場合
onMounted()
関数を利用して、ログイン状況を調べている間のローディング画面を設定します。middleware
で認証の判定を行うこともできますが, Vue composition API での実装がうまくいかなかったので、ログインしているかどうかの判定はlayouts/default.vue
に実装しました。
<script lang="ts">
import { defineComponent, onMounted } from '@vue/composition-api'
import { provideGlobalState, injectGlobalState } from '@/utils/states/user'
import { auth } from '~/plugins/firebase.config'
export default defineComponent({
name: 'DefaultLayout',
setup(props: any, { root }) {
provideGlobalState()
const state = injectGlobalState()
onMounted(() => {
const publicPath = ["/signin","/signup"]
if (state.user.value.id === '') {
auth.onAuthStateChanged((user) => {
if (user) {
state.setUserState({
id: user ? user.uid : '',
email: user && user.email ? user.email : '',
name: user && user.displayName ? user.displayName : '',
thumbnail: user && user.photoURL ? user.photoURL : '',
})
} else {
root.$router.push('/signin')
}
})
}
})
},
})
</script>
トップページについて
コードは次の通りです。
<template>
<div class="container">
<div>
<Logo />
<h1 class="title">firebase-auth-sample</h1>
<div class="links">
<a
href="https://nuxtjs.org/"
target="_blank"
rel="noopener noreferrer"
class="button--green"
>
Documentation
</a>
<a
href="https://github.com/nuxt/nuxt.js"
target="_blank"
rel="noopener noreferrer"
class="button--grey"
>
GitHub
</a>
<!--以下を追加-->
<button
target="_blank"
rel="noopener noreferrer"
class="button--grey"
@click="signout"
>
Signout
</button>
<!---->
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { SignOut } from '~/utils/firebase/auth'
import { injectGlobalState } from '~/utils/states/user'
export default defineComponent({
name: 'IndexPage',
setup(props: any, { root }) {
const state = injectGlobalState()
const signout = () => {
SignOut()
.then(() => {
state.cleanUserState()
})
.catch(() => {
alert('ログアウトに失敗しました。')
})
}
return {
signout,
}
},
})
</script>
<style>
.container {
margin: 0 auto;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.title {
font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: block;
font-weight: 300;
font-size: 100px;
color: #35495e;
letter-spacing: 1px;
}
.subtitle {
font-weight: 300;
font-size: 42px;
color: #526488;
word-spacing: 5px;
padding-bottom: 15px;
}
.links {
padding-top: 15px;
}
</style>
最後に
この記事では、Firebase Authentication を利用した認証機能をNuxt.js で行いましたが、この実装法は、Vue や React などの様々なフレームワークでも適用することは可能です。その際はぜひこの記事を参考にしてください。