【Script SetUp】Vue3でログイン機能をつけたい！【Firebase Authentication】【TypeScript】

# 本記事の内容
本記事ではScript SetUp、FireBase  Authenticationを用いたログイン機能の実装を行います。

実装する機能は以下になります。

* アカウント登録
* ログイン
* ログアウト
* 未ログイン時、中の情報を見れないようにする

# 事前準備
事前準備として、以下の二つがあります。
* axiosのインストール
* 型定義

## axiosのインストール
手順は以下の2つです。
①axiosのインストール
②axiosのインポート

### axiosのインストール
以下のコマンドを実行し、axiosをインストールしてください
```:
npm i axios
```

### axiosのインポート
完了したらaxiosをインポートします。
この際、必要なURLを省略できるよう、baseURLの設定も行なっておきます。

```typescript:src/axios/index.ts
import axios from "axios";

export const axiosComments = axios.create({
    //URL設定
    baseURL:
    'https://firestore.googleapis.com/v1/projects/vuejs-http-f75d6/databases/(default)/documents'
})

export const axiosAuth = axios.create({
    baseURL:
    'https://identitytoolkit.googleapis.com/v1'
})

export const axiosRefresh = axios.create({
    baseURL:
    'https://securetoken.googleapis.com/v1'
})
```
## 型定義
```typescript:src/types/index.ts
//vuexのstate用型定義
export type AuthState = {
    idToken: null
};

//ログインの情報送信用型定義
export type AuthRequest = {
    email: string;
    password: string;
    returnSecureToken: boolean;
};

//ログインのレスポンスデータ用型定義
export type AuthResponse = {
    idToken: string;
    refreshToken: string;
    expiresIn: number;
};
//ログイン継続のレスポンスデータ用型定義
export type RefreshResponse = {
    id_token: string;
    refresh_token: string;
    expires_in: number;
};
```
# Firebase Authenticationを使用したログイン処理
前述の通り、実装したい機能は以下の4つです。
* アカウント登録
* ログイン
* ログアウト
* 未ログイン時、中の情報を見れないようにする

## アカウント登録/ログイン/ログアウト
ここでは、タイトル通り、アカウント登録・ログイン・ログアウトの機能を実装します。
そのためには以下の4つの処理が必要です。

①登録情報をサーバーに渡し、必要なデータをローカルストレージに保存

②ログイン時データをサーバーに渡し、必要なデータをローカルストレージに保存、ページを遷移させる処理

③ログインに必要なidTokenの有効期限が切れた時に、必要なトークン情報を取得し、ローカルストレージに保存する処理。

④ログアウト時、ローカルストレージに保存したデータを全て削除し、ログイン画面に遷移する処理

それぞれより具体的には以下の処理をする必要があります。
### ①登録情報をサーバーに渡し、必要なデータをローカルストレージに保存
* データをサーバーに送信
* レスポンスデータを受け取り、必要なデータを保存する処理

### ②ログイン時データをサーバーに渡し、必要なデータをローカルストレージに保存、ページを遷移させる処理
* データをサーバーに送信
* レスポンスデータを受け取り、必要なデータを保存する処理
* ページ遷移

### ③ログインに必要なidTokenの有効期限が切れた時に、必要なトークン情報を取得し、ローカルストレージに保存する処理。
* 有効期限の計算
* 切れた際にidTokenの代わりとなるrefreshTokenを取得する処理
* ローカルストレージに必要なデータを保存する処理

### ④ログアウト時、ローカルストレージに保存したデータを全て削除し、ログイン画面に遷移する処理
* idTokenをなくす処理
* ローカルストレージに保存してあるデータを削除する処理
* ログイン画面に遷移する処理

ではコードを見ていきましょう。

ちなみに、axiosを使用する際に渡す、もしくは取得するデータに関しては[こちら](https://firebase.google.com/docs/reference/rest/auth)をご覧ください。

```typescript: src/store/auth/index.ts
import { Module, ActionTree, MutationTree, GetterTree } from "vuex";
import { AuthState, Auth } from "../../types";
import { RootState } from "../../types/rootstate";
import { axiosAuth, axiosRefresh } from '../../axios';
import router from "../../router"
import { AxiosResponse } from "axios";

const state: AuthState = {
    idToken : ''
};
const getters: GetterTree<AuthState, RootState> = {
    idToken: state => state.idToken
};

const mutations: MutationTree<AuthState> = {
    updateIdToken(state, idToken: string) {
        state.idToken = idToken;
    }
};

const actions: ActionTree<AuthState, RootState> = {
    //③ログインに必要なidTokenの有効期限が切れた時に、必要なトークン情報を取得し、ローカルストレージに保存する処理。
    async autoLogin({commit, dispatch}){
        const idToken = localStorage.getItem('idToken')
        //もしidTokenがなかったら何もしない
        if (!idToken) return;
        const now = new Date()
        const expiryTimeMs = localStorage.getItem('expiryTimeMs')
        // 有効期限が切れているとき値を定める
        const isExpired = now.getTime() >= (+expiryTimeMs!);
        const refreshToken = localStorage.getItem('refreshToken');
        if (isExpired) {
          await dispatch('refreshIdToken', refreshToken);
        } else {
          //有効期限の残り時間を計算して、その時間経ったらrefreshTokenを発行させる処理。
          const expiresInMs = +expiryTimeMs! - now.getTime();
          setTimeout(() => {
            dispatch('refreshIdToken', refreshToken);
          }, expiresInMs);
          commit('updateIdToken', idToken);
        }
      },

      //②ログイン時データをサーバーに渡し、必要なデータをローカルストレージに保存、ページを遷移させる処理
      login({dispatch}, authData: AuthRequest){
        //サーバーへ以下の情報を送信
        axiosAuth
        .post(
            '/accounts:signInWithPassword?key=AIzaSyB_4rHWZuPdElvwvC3jSCsjZr54H1_0fPg',
            {
                email: authData.email,
                password: authData.password,
                returnSecureToken: true
            }
        )
        //レスポンスを受け取る
        .then((response: AxiosResponse<AuthResponse>)=>{
        //コミットして、受け取った値を渡す。
          dispatch('setAuthData', {
            idToken: response.data.idToken,
            expiresIn: response.data.expiresIn,
            refreshToken: response.data.refreshToken
          });
          //ページ遷移
          router.push('/')
        }) 
      },
    
      //④ログアウト時、ローカルストレージに保存したデータを全て削除し、ログイン画面に遷移する処理
      logout({ commit }) {
        commit('updateIdToken', null);
        localStorage.removeItem('idToken');
        localStorage.removeItem('expiryTimeMs');
        localStorage.removeItem('refreshToken');
        router.replace('/login');
      },
      //リフレッシュトークンを返し、継続的に利用できるようにする関数
      async refreshIdToken({dispatch}, refreshToken: string){
        await axiosRefresh
        .post('/token?key=AIzaSyB_4rHWZuPdElvwvC3jSCsjZr54H1_0fPg',{
          grant_type: 'refresh_token',
          refresh_token: refreshToken
        })
        .then((response: AxiosResponse<RefreshResponse>) =>{
          dispatch('setAuthData', {
            idToken: response.data.id_token,
            expiresIn: response.data.expires_in,
            refreshToken: response.data.refresh_token
          });
        })
      },

    //①登録情報をサーバーに渡し、必要なデータをローカルストレージに保存
      register({dispatch}, authData: AuthRequest){
        axiosAuth
        .post(
          '/accounts:signUp?key=AIzaSyB_4rHWZuPdElvwvC3jSCsjZr54H1_0fPg',
            {
                email: authData.email,
                password: authData.password,
                returnSecureToken: true
            }
        )
        .then((response: AxiosResponse<AuthResponse>)=>{
          dispatch('setAuthData', {
            idToken: response.data.idToken,
            expiresIn: response.data.expiresIn,
            refreshToken: response.data.refreshToken
          });
        }) 
      },
      //ローカルステージにidTokenとexpiryTImeMsを保存することによって、永続的にログイン状態でいられる。
      setAuthData({ commit, dispatch }, authData: AuthResponse) {
        const now = new Date();
        //有効期限が切れるときの秒数を定義
        const expiryTimeMs = now.getTime() + authData.expiresIn * 1000;
        commit('updateIdToken', authData.idToken);
        //ローカルストレージに保存
        localStorage.setItem('idToken', authData.idToken);
        localStorage.setItem('expiryTimeMs', expiryTimeMs.toString());
        localStorage.setItem('refreshToken', authData.refreshToken);
        //トークンの有効期限後、リフレッシュトークンを返し、継続的に利用できるようにする処理
        setTimeout(() => {
          dispatch('refreshIdToken', authData.refreshToken);
        }, authData.expiresIn * 1000);
      }
}

export const AuthModule: Module<AuthState, RootState> = {
    namespaced: true,
    state,
    getters,
    actions,
    mutations,
};
```
idTokenがある場合に、ログイン状態を保つため、以下のコードを追記します。
```typescript:src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { store, key } from './store'

//autoLoginを一番最初に実行するよう設定
store.dispatch('Auth/autoLogin').then(() => {
    createApp(App).use(store, key).use(router).mount('#app')
})
```
## 未ログイン時、中の情報をブロックする処理
以下のコードをご覧ください。
```typescript:src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import TodoListView from '../views/TodoListView.vue'
import RegisterView from '../views/RegisterView.vue'
import LoginView from '../views/LoginView.vue'
import { store } from '../store'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/register',
    name: 'register',
    component: RegisterView
  },
  {
    path: '/login',
    name: 'login',
    component: LoginView
  },
  {
    path: '/,
    name: 'todo',
    component: TodoListView,
    //追記
    beforeEnter(to, from, next) {
      if (store.getters['Auth/idToken']) {
        next();
      } else {
        next('/login');
      }
    }
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
```

## ナビゲーションバーの変更
最後に、ログイン状態によってナビゲーションバーも変化する処理を行います。
```src/App.vue
<script setup lang="ts">
import { computed } from 'vue'
import { useStore } from './store'

  const store = useStore()
  //idTokenがあれば、認証されていると判断する算出プロパティ
  const idAuthenticated = computed(()=>{
    return store.getters['Auth/idToken']!= ''
  })

  const logout = () => {
    store.dispatch('Auth/logout');
  }
</script>
<template>
  <nav>
    <template v-if="!idAuthenticated" >
      <router-link to="/register" class="header-item">Register</router-link> |
      <router-link to="/login" class="header-item">Login</router-link> |
    </template>
    <template v-if="idAuthenticated" >
      <router-link to="/">Kanban-Board</router-link> |
      <span to="/" class="header-item" @click="logout" >Logout</span> |
    </template>
    
  </nav>
  <router-view/>
</template>
```