Next.js、TypeScript、FirebaseAuthでメール認証(確認メールつき)

## 環境
```bash
$ yarn -v
1.22.10
```
## 作成
### Firebase側の設定
この辺の記事を参考に新規プロジェクトを作成し、Authの設定でメールを有効化。
https://webbibouroku.com/Blog/Article/firebase-auth

### Next.jsプロジェクト開始
```
$ yarn create next-app sample_app
$ cd sample_app
```
### TypeScriptを導入
```bash
$ touch tsconfig.json
$ yarn add --dev typescript @types/react @types/node
$ mv pages/_app.js pages/_app.tsx
$ mv pages/index.js pages/index.tsx
$ yarn dev
```
### 環境変数をセット
```bash
$ touch .env
```
```:.env(各自Firebaseで取得した値をセットする)
FIREBASE_KEY=AIzaSyC7PpE2RvbHohTN292P
FIREBASE_DOMAIN=scr.firebaseapp.com
FIREBASE_PROJECT_ID=scra
FIREBASE_STORAGE_BUCKET=stips.appspot.com
FIREBASE_SENDER_ID=10800240
FIREBASE_APP_ID=1:108200240:web:f6d1bd8762b07575
FIREBASE_MEASUREMENT_ID=G-9ZD3QCY
```
```bash
$ touch next.config.js
```
```next.config.js
module.exports = {
  env: {
    FIREBASE_KEY: process.env.FIREBASE_KEY,
    FIREBASE_DOMAIN: process.env.FIREBASE_DOMAIN,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
    FIREBASE_SENDER_ID: process.env.FIREBASE_SENDER_ID,
    FIREBASE_APPID: process.env.FIREBASE_APPID
  }
}
```

### FirebaseAuthの設定
```bash
$ yarn add firebase
$ mkdir utils
$ touch utils/firebase.ts
```
```src/utils/firebase.ts
import 'firebase/auth'
import 'firebase/firestore'

import firebase from 'firebase/app'

const config = {
  apiKey: process.env.FIREBASE_KEY,
  authDomain: process.env.FIREBASE_DOMAIN,
  databeseURL: process.env.FIREBASE_DATABASE,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_SENDER_ID,
  appId: process.env.FIREBASE_APPID,
}
// initializeを複数回走らせない
if (firebase.apps.length === 0) {
  firebase.initializeApp(config)
}
const auth = firebase.auth()
export { auth }
```
```bash
$ mkdir auth
$ touch auth/AuthProvider.tsx
```
```src/auth/AuthProvider.tsx
import { User } from 'firebase'
import { FC, createContext, useEffect, useState } from 'react'
import { auth } from '../utils/firebase'

type AuthContextProps = {
  currentUser: User | null | undefined
}

const AuthContext = createContext<AuthContextProps>({ currentUser: undefined })

const AuthProvider: FC = ({ children }) => {
  const [currentUser, setCurrentUser] = useState<User | null | undefined>(
    undefined
  )

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      setCurrentUser(user)
    })
  }, [])

  return (
    <AuthContext.Provider value={{ currentUser }}>
      {children}
    </AuthContext.Provider>
  )
}

export { AuthContext, AuthProvider }
```

### ログイン画面を作成
```bash
$ touch pages/login.tsx
```
```tsx:src/pages/login.tsx
import React, { useEffect, useState, FC } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { auth } from '../utils/firebase'

const Login: FC = () => {
  const router = useRouter()
  const [email, setEmail] = useState<string>('')
  const [password, setPassword] = useState<string>('')

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push('/')
    })
  }, [])

  const logIn = async (e) => {
    e.preventDefault()
    try {
      await auth.signInWithEmailAndPassword(email, password)
      router.push('/')
    } catch (err) {
      alert(err.message)
    }
  }

  return (
    <div>
      <form onSubmit={logIn}>
        <div>
          <label htmlFor="email">
            Email:{' '}
          </label>
          <input
            id="email"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">
            Password:{' '}
          </label>
          <input
            id="password"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">
          Login
        </button>
      </form>
      <Link href="/signup">
        <a>signup</a>
      </Link>
    </div>
  )
}

export default Login
```

### サインアップ画面を作成
```bash
$ touch pages/signup.tsx
```
```tsx:signup.tsx
import { FC, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'

import { auth } from '../utils/firebase'

const SignUp: FC = () => {
  const router = useRouter()
  const [email, setEmail] = useState<string>('')
  const [password, setPassword] = useState<string>('')

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push('/')
    })
  }, [])

  const createUser = async (e) => {
    e.preventDefault()
    try {
      await auth.createUserWithEmailAndPassword(email, password)
      await auth.currentUser.sendEmailVerification()
      router.push('/sent')
    } catch (err) {
      alert(err.message)
    }
  }

  return (
    <div>
      <form onSubmit={createUser}>
        <div>
          <label htmlFor="email">
            Email:{' '}
          </label>
          <input
            id="email"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">
            Password:{' '}
          </label>
          <input
            id="password"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">
          SignUp
        </button>
      </form>
      <Link href="/login">
        <a>Login</a>
      </Link>
    </div>
  )
}

export default SignUp
```

### メール送信後画面を作成
```bash
$ touch pages/sent.tsx
```
```tsx:pages/sent.tsx
import { FC, useEffect } from 'react'
import { useRouter } from 'next/router'

import { auth } from '../utils/firebase'

const SignUp: FC = () => {
  const router = useRouter()

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user.emailVerified && router.push('/')
    })
  }, [])

  const sendEmailVerification = async () => {
    try {
      await auth.currentUser.sendEmailVerification()
      alert('認証メールを再送しました')
      router.push('/sent')
    } catch (err) {
      alert(err.message)
    }
  }

  return (
    <div>
      <p>メール内のリンクをクリックしてユーザーを有効化してください。</p>
      <button onClick={sendEmailVerification}>再送する</button>
    </div>
  )
}

export default SignUp
```

### トップページを作成
```tsx:pages/index.tsx
import { useEffect, FC, useState } from 'react'
import { useRouter } from 'next/router'

import { auth } from '../utils/firebase'

const Home: FC = (props: any) => {
  const router = useRouter()
  const [currentUser, setCurrentUser] = useState<null | object>(null)

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user ? setCurrentUser(user) : router.push('/login')
      !user.emailVerified && router.push('/sent')
    })
  }, [])

  const logOut = async () => {
    try {
      await auth.signOut()
      router.push('/login')
    } catch (error) {
      alert(error.message)
    }
  }

  return (
    <div>
      <pre>{currentUser && JSON.stringify(currentUser, null, 4)}</pre>
      <button onClick={logOut}>Logout</button>
    </div>
  )
}

export default Home
```

## 参考
- [今更ながらのNext.js + TypeScript + Firebaseで認証機能を実装する](https://zenn.dev/k_logic24/articles/react-auth-with-firebase)