「Go×Firebase×React」でJWT認証できる簡単なTODOアプリ作ってみた

こんにちは。初投稿です。
来年から新卒でエンジニアになる予定の者です。

## 初めに
実務未経験、独学ですので、何か勘違いがあるかもしれないということを予めご了承ください。間違いがあれば指摘していただけますと幸いです。

またこの記事ではDockerやReactなどの詳しいことは触れていません。

## この記事でやること
Go,Reactを学習したので習作物としてバックエンドGo,フロントエンドReactを使ったFirebase×JWT認証つきの簡単なTODOアプリを開発しました。

簡単にJWT(Json Web Token)だけ説明しますと、
* 認証用のトークンとして用いられる
* JSON形式でやり取りするため任意の情報を含むことができる
* 改ざんを検知できる
などの特徴があります

https://jwt.io/

こちらの記事がハンズオン形式でわかりやすかったです。↓

https://qiita.com/po3rin/items/740445d21487dfcb5d9f

# 完成イメージ
![TODO操作デモ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1726236/c7014515-67c2-fd51-d8ca-ecdc38eab793.gif)
今回書いたコードは[こちら](https://github.com/fumi7649/go-react-todo)に公開しました。
## 開発環境
開発環境はWSL2でUbuntu20.04とDocker-Desktop(4.10.1)を使っています
## GoでWeb API開発
まずGoでWebAPIを作っていきます.
APIの開発にはこちらを参考にさせていただきました。
[nginx + golangでWeb API開発](https://qiita.com/unokun/items/dcf3ed4fbc4f532f664a)
REST APIについて　[0からREST APIについて調べてみた](https://qiita.com/masato44gm/items/dffb8281536ad321fb08)

簡単なディレクトリ構成は以下のようになっています。参考までに
```
/go-react-todo ルートディレクトリ
    ├── api
    ├── docker
    │   ├── go
    │   ├── mysql
    │   └── react
    └── front
        └── todoapp
```
goとmysqlのDockerfileを書いていきます。
```Dockerfile
FROM golang:1.19.2-alpine3.16
RUN apk update && apk add git 
WORKDIR /go/src/app
COPY ./api /go/src/app
```
```Dockerfile
FROM --platform=linux/x86_64 mysql:latest
```


### データベース接続
続いてデータベースの設定を行っていきます。
まずDockerのMySQLコンテナが起動したときに初期データを投入するようにしておきます。[参考 Docker MySQLコンテナ起動時に初期データを投入する](https://qiita.com/NagaokaKenichi/items/ae037963b33a85df33f5)
```docker-compose.yml
mysql:
    build: 
      context: .
      dockerfile: ./docker/mysql/Dockerfile
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: tododb
      MYSQL_PASSWORD: root
      TZ: 'Asia/Tokyo'
    volumes:
     - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d
    ports:
      - 3307:3307
    command: --port 3307
    links:
     - go
```
docker/mysqlの下にinitdb.dというディレクトリを作成して、その中にtodos.sqlを作成します。

```docker/mysql/initdb.d/todos.sql
CREATE SCHEMA IF NOT EXISTS tododb;
USE tododb;
DROP TABLE IF EXISTS todos;

CREATE TABLE IF NOT  EXISTS todos (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  title VARCHAR(255) NOT NULL,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL,
  PRIMARY KEY (id)
);
```
これでMysqlコンテナを起動したとにデータベースとテーブルが作成されます。
初期データを入れたければtodos.sql内に
```
INSERT INTO todos (titile) VALUES ("お掃除");
```
みたいな感じで続けば初期データも登録できます。

続いてgoでmysqlに接続していきます。
modelディレクトリの中にdatabase.goを作成してその中にデータベースとの接続処理を書いていきます。

```/api/model/database.go
package model

import (
	"database/sql"
	"log"
	_ "github.com/go-sql-driver/mysql"
)


var Db *sql.DB
var err error

func DbConnect() {
	dsn := "root:root@tcp(mysql:3307)/tododb?charset=utf8&parseTime=true"
	Db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalln(err)
	}
}
```
dnsはこのように記述します↓[参考](https://qiita.com/rmanzoku/items/e2a1d50c58dedba3f5a9)
```
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
```

database.goで定義したDbConnect関数をmain.goのinit関数で実行してDbに値を入れていきます。

```/api/main.go
package main

import (
	"github.com/fumi7649/go-react-todo/model"
	"github.com/fumi7649/go-react-todo/utils"
)

func init () {
	utils.LoggingSettings("todoapp.log")
	model.DbConnect()
}
```
### model
続いてデータベース操作の処理を書いていきます。

```model/todo.go
package model

import (
	"log"
	"time"
)

type Todo struct {
	ID uint `json:"id"`
	Title string `json:"title"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}


func GetTodo(id uint) (todo Todo, err error) {
	err = Db.QueryRow("SELECT id, title, created_at, updated_at FROM todos WHERE id = ?", id).Scan(
		&todo.ID,
		&todo.Title,
		&todo.CreatedAt,
		&todo.UpdatedAt,
	)
	if err != nil {
		log.Fatalln(err)
	}
	return todo, err
}

func GetTodos() (todos []Todo, err error) {
	rows, err := Db.Query("SELECT id, title, created_at, updated_at FROM todos")
	if err != nil {
		log.Fatalln(err)
	}
	defer rows.Close()

	for rows.Next() {
		var todo Todo
		err = rows.Scan(
			&todo.ID,
			&todo.Title,
			&todo.CreatedAt,
			&todo.UpdatedAt,
		)
		todos = append(todos, todo)
	}

	return todos, err
}

func CreateTodo(title string) (err error) {
	now := time.Now()
	_, err = Db.Exec("INSERT INTO todos (title, created_at, updated_at) VALUES(?, ?, ?)", title, now, now)
	if err != nil {
		log.Fatalln(err)
	}
	return err
}


func (todo *Todo) UpdateTodo() error {
	_, err = Db.Exec("update todos set title = ? where id = ?", todo.Title, todo.ID)
	if err != nil {
		log.Fatalln(err)
	}
	return err
}

func (todo *Todo) DeleteTodo() error {
	_, err = Db.Exec("delete from todos where id = ?", todo.ID)
	if err != nil {
		log.Fatalln(err)
	}
	return err
}
```
これでgoからデータベースを操作できるようになりました。

### Controller
続いてルーティングを行っていきます。フレームワークは[gin](https://github.com/gin-gonic/gin)を使ってます。

```api/controller/router.go
package controller

import (
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/fumi7649/go-react-todo/model"
	"github.com/gin-gonic/gin"
)

func StartServer() {
	router := gin.Default()
	
	v1 := router.Group("todo/api/v1")
	{
		v1.GET("/todos", todosGET)
		v1.POST("/todos", todoPOST)
		v1.PATCH("/todos/:id", todoPATCH)
		v1.DELETE("/todos/:id", todoDELETE)
	}
	router.Run(":8080")
}

func todosGET(c *gin.Context) {
	todos, err := model.GetTodos()
	if err != nil {
		log.Fatalln("err")
	}
	c.JSON(http.StatusOK, gin.H{"todos": todos})
}

func todoPOST(c *gin.Context) {
	title := c.PostForm("title")
	err := model.CreateTodo(title)
	if err != nil {
		log.Fatalln(err)
	}
}

func todoPATCH(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		log.Fatalln(err)
	}
	todo, err := model.GetTodo(uint(id))
	if err != nil {
		log.Fatalln(err)
	}

	title := c.PostForm("title")
	now := time.Now()
	todo.Title = title
	todo.UpdatedAt = now
	todo.UpdateTodo()
	c.JSON(http.StatusOK, gin.H{"todo": todo})
}

func todoDELETE(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		log.Fatalln(err)
	}
	todo, err := model.GetTodo(uint(id))
	if err != nil {
		log.Fatalln(err)
	}
	err = todo.DeleteTodo()
	if err != nil {
		log.Fatalln(err)
	}
	c.JSON(http.StatusOK, "Deleted")
}
```
main.goでStartServer()を実行してサーバーを立ち上げます。
```main.go
package main

import (
	"github.com/fumi7649/go-react-todo/controller"
	"github.com/fumi7649/go-react-todo/model"
	"github.com/fumi7649/go-react-todo/utils"
)

func init () {
	utils.LoggingSettings("todoapp.log")
	model.DbConnect()
}

func main () {
	controller.StartServer()
}
```
ここまででざっくりとAPIは完成です。
[Restlet Client](https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ja)というChromeの拡張機能を使って作ったAPIを呼び出してみます。

```
$docker-compose up -d
$docker-compose exec go sh //goコンテナの中に入る
/go/src/app # go run main.go //サーバー起動
```

![api.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1726236/9ab7d4d0-758d-fd3f-5ebe-364d18639b2e.jpeg)
![apiget.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1726236/6d340831-4b6e-dde3-8976-15c7d45c6169.jpeg)
呼び出せますね！


# ReactでUIを作る

Dockerfileから書いていきましょう。
Reactでの環境構築は[こちら](https://zenn.dev/rihito/articles/96dfad8d4990f9)を参考にしました。

```Dockerfile
FROM  node:18.12.0-alpine3.16
WORKDIR /usr/src/app
COPY ./front /usr/src/app
```
```docker-compose.yml
react:
    build: 
      dockerfile: ./docker/react/Dockerfile
    volumes:
      - ./front:/usr/src/app
    command: sh -c "cd todoapp && npm start"
    ports:
     - "3000:3000"
    stdin_open: true
```
以下のコマンドでプロジェクトを作成
```
$ docker-compose rum --rm react sh -c "npm install -g create-react-app && create-react-app todoapp"
```
```
$ docker-compose up -d react
```
localhost:3000にアクセスするとプロジェクトが立ち上がっています。
![ReactLocal.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1726236/d6b2c29a-09b9-13c6-8099-6edffaa85938.jpeg)

今回は[Mui UI](https://mui.com/)を使います。
コンテナを起動してコンテナの中に入ってライブラリをインストールしていきます。
```
$ docker-compose up -d react 
$ docker-compose exec react sh
/usr/src/app cd todoapp
/usr/src/app/todoapp npm install @mui/material @emotion/react @emotion/styled
/usr/src/app/todoapp npm install @mui/icons-material
```
後でiconも使いたいので@mui/icons-materialもインストールしておきます。
いい感じのiconがたくさんあります⇒[Material Icons](https://mui.com/material-ui/material-icons/)
## Signupページ
```front/todoapp/src/components/auth/Signup.js
import { Grid, Paper, Typography, TextField, Link, Button, Box } from '@mui/material';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';


const Signup = () => {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleChangeEmail = (event) => {
    setEmail(event.target.value);
  }

  const handleChangePassword = (event) => {
    setPassword(event.target.value);
  }

  const handleSubmit = () => {
    navigate("/");
 } 
  return (
    <Grid>
      <Paper
        elevation={3}
        sx={{
          p: 4,
          height: "70vh",
          width: "50vh",
          m: " 20px auto"
        }}
      >
        <Typography variant={"h5"} sx={{ m: "30px" }}>Sign up</Typography>
        <TextField
          label="Email"
          variant="standard"
          fullWidth
          required
          value={email}
          onChange={handleChangeEmail}
        />
        <TextField
          type="password"
          label="Password"
          variant="standard"
          fullWidth
          value={password}
          onChange={handleChangePassword}
          required />
        <Box mt={4}>
          <Button type="submit" color="primary" variant="contained" fullWidth onClick={handleSubmit}>
            登録
          </Button>
          <Typography variant="caption">
            <Link href="/signin">アカウントをお持ちですか？</Link>
          </Typography>
        </Box>
      </Paper>
    </Grid>
  );
}

export default Signup;
```
## Signinページ
```front/todoapp/src/components/auth/Signin.js
import { Grid, Paper, Typography, TextField, Link, Button, Box } from '@mui/material';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const Signin = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const navigate = useNavigate();

  const handleChangeEmail = (event) => {
    setEmail(event.target.value);
  }

  const handleChangePassword = (event) => {
    setPassword(event.target.value);
  }

  const handleSubmit = () => {
    navigate('/');
  }
  return (
    <Grid>
      <Paper
        elevation={3}
        sx={{
          p: 4,
          height: "70vh",
          width: "50vh",
          m: " 20px auto"
        }}
      >
        <Typography variant={"h5"} sx={{ m: "30px" }}>Sign in</Typography>
        <TextField
          label="Email"
          variant="standard"
          fullWidth
          required
          value={email}
          onChange={handleChangeEmail} />
        <TextField
          type="password"
          label="Password"
          variant="standard"
          fullWidth
          value={password}
          onChange={handleChangePassword}
          required />
        <Box mt={4}>
          <Button type="submit" color="primary" variant="contained" fullWidth onClick={handleSubmit}>
            ログイン
          </Button>
          <Typography variant="caption">
            アカウントを持っていませんか？
            <Link href="#">アカウント作成</Link>
          </Typography>
        </Box>
      </Paper>
    </Grid>
  );
}

export default Signin;
```
## Topページ
```front/todoapp/src/components/views/Top.js
import Header from "./Header";
import TodoList from "./TodoList";

const Top = () => {
  return (
    <>
      <Header/>
      <TodoList/>
    </>
  )
}

export default Top;
```
## TodoList & TodoItem
```front/todoapp/src/components/views/TodoList.js
import { Button, Grid, TextField } from '@mui/material';
import { useState } from 'react';
import AddIcon from '@mui/icons-material/Add';
import TodoItem from './TodoItem';

const TodoList = () => {
  const [todo, setTodo] = useState('');
  const [todos, setTodos] = useState([]);

  const handleChangeTodo = (event) => {
    setTodo(event.target.value);
  }

  return (
    <>
      <Grid container alignItems="center" justifyContent="center" sx={{ m: 3 }}>
        <TextField
          label="todo"
          name='todo'
          value={todo}
          type="text"
          variant="outlined"
          size='small'
          onChange={handleChangeTodo}
        />
        <Button variant="contained" endIcon={<AddIcon />}>create</Button>
      </Grid>
      <TodoItem {...todos}/>
    </>
  );
}

export default TodoList;
```

```front/todoapp/src/components/views/TodoItem.js
import { Button, Grid, List, ListItem, ListItemText } from "@mui/material"
import { Box } from "@mui/system";
import WorkIcon from '@mui/icons-material/Work';
import DeleteIcon from '@mui/icons-material/Delete';


const TodoItem = (props) => {
  const todos = props.todos;
  return (
    <Grid container alignItems="center" justifyContent="center">
      <Box
        sx={{ width: '100%', height: 400, maxWidth: 360, bgcolor: 'background.paper' }}>
        <List
          sx={{
            width: '100%',
            maxWidth: 360,
            bgcolor: 'background.paper',
            position: 'relative',
            overflow: 'auto',
            maxHeight: 300,
            '& ul': { padding: 0 },
          }}>
          {todos?.map((todo, index) => {
            return (
              <ListItem key={index}>
                <WorkIcon sx={{ p: 2 }} />
                <ListItemText primary={todo.title} />
                <Button onClick={() => props.onClick(todo.id)} endIcon={<DeleteIcon />}>Delete</Button>
              </ListItem>
            )
          })}
        </List>
      </Box>
    </Grid>
  );
}

export default TodoItem;
```
create-react-appコマンドを実行したときにデフォルトで生成されるApp.jsを再活用してフロントでルーティングを行います。
```front/todoapp/src/App.js
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Signin from "./components/auth/Signin";
import Signup from "./components/auth/Signup";
import Top from "./components/views/Top";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Top />} />
        <Route path="/signup" element={<Signup />} />
        <Route path="/signin" element={<Signin />} />
      </Routes>
    </BrowserRouter>
  )
}

export default App;
```
## React側でAPIをたたく
React側の処理を書いていく前に、Go側でCORSの設定をしておきます。CORSについては[こちらの記事](https://qiita.com/att55/items/2154a8aad8bf1409db2b)が分かりやすかったです。

ざっくり説明するとセキュリティ上サーバ―側で、アクセスするクライアント側のサイトを許可する必要があります。今回は**github.com/gin-contrib/cors**で設定していきます。

```api/controller/router.go
package controller

import (
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/fumi7649/go-react-todo/model"
	"github.com/gin-contrib/cors" //新しく追加
	"github.com/gin-gonic/gin"
)

func StartServer() {
	router := gin.Default()

// CORSの設定
	router.Use(cors.New(cors.Config{
		AllowOrigins: []string{
			"http://localhost:3000",
		},
		AllowMethods: []string{
			"GET",
			"POST",
			"DELETE",
			"PUT",
		},
		AllowHeaders: []string{
			"Authorization",
		},
	}))
	v1 := router.Group("/todo/api/v1")
	{
		v1.GET("/todos", todosGET)
		v1.POST("/todos", todoPOST)
		v1.PATCH("/todos/:id", todoPATCH)
		v1.DELETE("/todos/:id", todoDELETE)
	}
	router.Run(":8080")
}
// ... 以下省略
```



APIをたたくために[axios](https://github.com/axios/axios)を導入します。
MuiUIを導入したときのようにコンテナ内に入ってインストールします。

```
$ npm install axios
```
### todosデータ取得
goで作ったAPIからtodosデータを取得して表示します。

```TodoList.js
import { Button, Grid, TextField } from '@mui/material';
import { useEffect, useState } from 'react';
import AddIcon from '@mui/icons-material/Add';
import TodoItem from './TodoItem';
import axios from 'axios'; //新しく追加

const TodoList = () => {
  const [todo, setTodo] = useState('');
  const [todos, setTodos] = useState([]);

  const handleChangeTodo = (event) => {
    setTodo(event.target.value);
  }

// ... 追加
  const getTodosData = async() => {
    await axios.get("http://localhost:8080/todo/api/v1/todos")
      .then((res) => {
        setTodos(res.data);
      }).catch((err) => {
        console.error(err);
      })
  }
  
  useEffect(() => {
    getTodosData();
  }, []);

// ... 省略

export default TodoList;
```
```getTodosData()```でtodosに値を入れます。Reactの```useEffect()```を使って描画終了後にgetTodosDataを実行できるようにしています。

すべてのコンテナを立ち上げてlocalhost:3000にアクセスしてみます。

```
$ docker-compose up -d
```
データベースにデータが入っていると以下のように表示されると思います。
当たり前ですが、まだ**CREATE**や**DELETE**を押しても反応しないので処理を追加していきます。
![getTodosDataデモ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1726236/5498c172-886b-09bd-a678-b9ab427fc7f5.jpeg)
## 作成・削除機能の追加

```TodoList.js

// ...
const TodoList = () => {
  const [todo, setTodo] = useState('');
  const [todos, setTodos] = useState([]);

  const handleChangeTodo = (event) => {
    setTodo(event.target.value);
  }

  const getTodosData = async () => {
    await axios.get("http://localhost:8080/todo/api/v1/todos")
      .then((res) => {
        setTodos(res.data);
      }).catch((err) => {
        console.error(err);
      })
  }

  useEffect(() => {
    getTodosData();
  }, []);

// 追加
  const handleCreateTodo = async () => {
    const params = new URLSearchParams;
    params.append("title", todo);
    await axios.post("http://localhost:8080/todo/api/v1/todos", params)
      .then(() => {
        getTodosData();
        setTodo('');
      })
      .catch((err) => {
        console.error(err);
      });
    console.log("create todo");
  }

  const handleDeleteTodo = async (id) => {
    await axios.delete(`http://localhost:8080/todo/api/v1/todos/${id}`)
      .then(() => {
        getTodosData();
      })
      .catch((err) => {
        console.error(err);
      });
  }

// ...

  return (
    <>
      <Grid container alignItems="center" justifyContent="center" sx={{ m: 3 }}>
        <TextField
          label="todo"
          name='todo'
          value={todo}
          type="text"
          variant="outlined"
          size='small'
          onChange={handleChangeTodo}
        />
        <Button variant="contained" endIcon={<AddIcon />} onClick={ handleCreateTodo }>CREATE</Button>
// onClickにhandleCreateTodoを渡してクリックで実行
      </Grid>
// TodoItemにもhandleDeleteTodoを渡す
      <TodoItem {...todos} onClick={ handleDeleteTodo } />
    </>
  );
}

export default TodoList;
```
これでタスクを作ったり消したりできるようになりました。

# 認証機能追加
続いてメールアドレスを登録したり、ログインできるようにしていきます。
JWT認証機能を作るのにあたっては以下の記事をかなり参考にさせていただいてます。

https://qiita.com/po3rin/items/d3e016d01162e9d9de80

## Firebaseを設定

ReactでFirebaseを使うためにモジュールをインストールしておきます。
```
$ npm install firebase
```
[Firebase](https://firebase.google.com)でプロジェクトを作成します。

ウェブアプリに Firebase を追加をクリックしてプロジェクト名を記入します。

firebase.jsというファイルを作成して、その中にプロジェクトを作成したときに表示される設定項目をコピーしておきます。
また、今回はAuthenticationのメール認証を使うので、メール/パスワードを有効にしておきます。

```front/todoapp/src/components/auth/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: "your_key",
  authDomain: "your_domain",
  projectId: "your_id",
  storageBucket: "your_storageBucket",
  messagingSenderId: "your_messagingSenderId",
  appId: "your_appId"
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
```
認証機能に必要な```auth```変数も```getAuth()```で取得し公開しておきます。
### サインアップ機能
Signup.jsのhandleSubmitを編集してメールアドレスとパスワードをFirebaseに登録できるようにします。
```Signup.js
import { Grid, Paper, Typography, TextField, Link, Button, Box } from '@mui/material';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { createUserWithEmailAndPassword } from 'firebase/auth'; //新しく追加
import { auth } from './firebase'; //　新しく追加


const Signup = () => {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleChangeEmail = (event) => {
    setEmail(event.target.value);
  }

  const handleChangePassword = (event) => {
    setPassword(event.target.value);
  }

// 編集
  const handleSubmit = async () => {
    await createUserWithEmailAndPassword(auth, email, password)
      .then(() => {
        navigate("/");
      }).catch((err) => {
        alert(err.message);
        console.error(err);
      });
 } 

// 省略
```

```createUserWithEmailAndPassword()```とfirebase.jsで公開した```auth```を使ってユーザーを作成していきます。
ユーザーの作成が無事成功したら"/"に飛ぶようにしています。

### サインイン機能
サインアップ同様にSignin.jsでも同様に```handleSubmit```を編集していきます。

```Signin.js
import { Grid, Paper, Typography, TextField, Link, Button, Box } from '@mui/material';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { auth } from './firebase'; // 新しく追加
import { signInWithEmailAndPassword } from 'firebase/auth'; // 新しく追加

const Signin = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const navigate = useNavigate();

  const handleChangeEmail = (event) => {
    setEmail(event.target.value);
  }

  const handleChangePassword = (event) => {
    setPassword(event.target.value);
  }

// 編集
  const handleSubmit = async () => {
    await signInWithEmailAndPassword(auth, email, password)
      .then((userCredential) => {
        const user = userCredential.user;
        user.getIdToken().then(idToken => {
          localStorage.setItem('jwt', idToken.toString());
        });
        navigate("/");
      })
      .catch((err) => {
        alert(err.message);
        console.error(err);
      });
  };
// 省略
```
ログイン時に返ってきた情報　```userCredential```　からサーバー認証に使うJWT(idToken)をローカルストレージに保存しておきます。
これは後でJWT認証機能を追加するときに使います。

### サインアウト
Header.jsの```handleLogout```を編集します。
サインアウトは```auth.signOut()```を実行するだけです。
localStorageからJWTも削除し、"/signin"にリダイレクトするようにします。
```Header.js
import { AppBar, Box, Button, Toolbar, Typography } from "@mui/material";
import { useNavigate } from 'react-router-dom';
import { auth } from "../auth/firebase"; //　新しく追加

const Header = () => {
  const navigate = useNavigate();

// 編集
  const handleLogout = () => {
    auth.signOut()
      .then(() => {
        localStorage.removeItem('jwt');
        navigate("/signin");
      })
      .catch((err) => {
        alert(err.message);
        console.error(err);
      })
  };

// 省略
```

これでメールアドレスの登録やログインできるようになりましたが、今の状態だと直接"/"にアクセスすれば、わざわざログインしなくてもアクセスできてしまいます。
ですので、現在ユーザーがログインしているかどうかという情報を、アプリケーション内で保持する必要があります。

ReactではReact Hookの```useContext()```を使用するとコンポーネント間でpropsをやり取りしなくてもグローバルなデータを扱うことができます。

```useContext()```を使ったユーザー情報の共有は[こちら](https://reffect.co.jp/react/react-firebase-auth)を参考にしました。


AuthContext.jsというファイルを作成します。ここではユーザーの情報を共有するためのコンポーネントを書いています。
```front/todoapp/src/context/AuthContext.js
import { createContext, useContext, useEffect, useState } from "react";
import { auth } from "../components/auth/firebase";
const AuthContext = createContext();

export function useAuthContext() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [user, setUser] = useState('');
  const [loading, setLoading] = useState(true);
  const value = {
    user,
    loading,
  }

  useEffect(() => {
    const unsubscribed = auth.onAuthStateChanged((user) => {
      setUser(user);
      setLoading(false);
    });
    return () => {
      unsubscribed();
    };
  }, []);

  if (loading) {
    return <p> loading ... </p>
  } else {
    return <AuthContext.Provider value={value}>{!loading && children}</AuthContext.Provider>
  }
}
```

```auth.onAuthStateChanged()```を使うとユーザー情報を取得することができます。```useState()```でuser情報をセットします。


```App.js
// 省略

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Top />} />
          <Route path="/signup" element={<Signup />} />
          <Route path="/signin" element={<Signin />} />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  )
}
// 省略
```
App.jsでAuthProviderを読み込んで囲みます。

```Top.js
import React, { useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import { useAuthContext } from '../../context/Authcontext';
import TodoList from './TodoList';
import Header from './Header';

const Top = () => {
  const info = useAuthContext();

  if (!info.user) {
    return <Navigate to="/signin" />
  } else {
    return (
      <>
        <Header/>
        <TodoList/>
      </>
    );
  }
}
export default Top;
```

ここではユーザー情報を```useAuthContext()```で取得しています。info.userに情報が入っていないとき、"/"にアクセスすると"/signin"にリダイレクトされるように条件分岐しておきます。
これでログインしていないとき、"/"にアクセスできないようになりました。

## JWT認証でAPIサーバーをセキュアにする
現段階でGoで作ったAPIサーバーは誰でもたたけるようになっています。そこでこの記事の冒頭で紹介したJWTを使って、Reactで作ったアプリケーションでサインインしたユーザーのみがAPIをたたけるようにしたいと思います。

### 必要なパッケージの導入
```
$docker-compose up -d // docker起動
$docker-compose exec go sh // goコンテナに入る
/go/src/app # go get -u firebase.google.com/go
/go/src/app # go get -u google.golang.org/api/option
```

Firebase Admin SDK Goを使うためにFirebaseコンソールから認証情報が含まれるJSONファイル(鍵ファイル)をダウンロードする必要があります。詳しくは⇒[サーバーにFirebaseAdminSDKを追加します](https://firebase.google.com/docs/admin/setup?authuser=0)

### GoにJWT認証機能追加
Go側でcontrollerディレクトリの中にauth.goを作成します。auth.goにはJWT認証するためのミドルウェアを書いていきます。このミドルウェアでハンドラーをラップすればAPIに認証機能が追加されるようにします。
先ほど取得した鍵ファイルは環境変数にパスを記述することで読み込みます。Goでは```os.Getenv("環境変数")```で読み込むことができます。ginでのミドルウェアの書き方は[こちら](https://qiita.com/tobita0000/items/d2309cc3f0dd32006ead)を参考にしました。
```api/controller/auth.go
package controller

import (
	"context"
	"log"
	"net/http"
	"os"
	"strings"

	firebase "firebase.google.com/go"
	"github.com/gin-gonic/gin"
	"google.golang.org/api/option"
)

func authMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		opt := option.WithCredentialsFile(os.Getenv("GOOGLE_CREDENTIALS_JSON"))

		app, err := firebase.NewApp(context.Background(), nil, opt)
		if err != nil {
			log.Printf("err: %v\n", err)
			os.Exit(1)
		}
		auth, err := app.Auth(context.Background())
		if err != nil {
			log.Printf("err: %v\n", err)
			os.Exit(1)
		}

		authHandler := c.Request.Header.Get("Authorization")
		idToken := strings.Replace(authHandler, "Bearer ", "", 1)

		token, err := auth.VerifyIDToken(context.Background(), idToken)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"Error": err.Error(),
			})
			return
		}
		log.Printf("Vertifed ID token: %v\n", token)
		c.Next()
	}
}
```

今回はdockerを使っているのでコンテナ内でも鍵ファイルの認証を通す必要があります。docker-composeを編集することで、外部の環境変数(今回は```GOOGLE_CREDENTIALS_JSON```)をそのままコンテナに持っていくことができます。[参考 Docker 開発環境の中から GCP サービスへの認証を通すかんたんなやり方](https://qiita.com/castaneai/items/1a5c02c3031ef394b33e)
```docker-compose.yml
services:
  go:
     # ...　省略
    environment:
      GOOGLE_CREDENTIALS_JSON: /key.json
    volumes:
      - ${GOOGLE_CREDENTIALS_JSON}:/key.json:ro
      - ./api:/go/src/app
     # ... 省略
```
auth.goで書いたミドルウェアでラップします。
```api/controller/router.go
// ...　省略
func StartServer() {
	router := gin.Default()
	router.Use(cors.New(cors.Config{
		AllowOrigins: []string{
			"http://localhost:3000",
		},
		AllowMethods: []string{
			"GET",
			"POST",
			"DELETE",
			"PUT",
		},
		AllowHeaders: []string{
			"Authorization",
		},
	}))

	v1 := router.Group("/todo/api/v1")
	v1.Use(authMiddleware()) // 追加
	{
		v1.GET("/todos", todosGET)
		v1.POST("/todos", todoPOST)
		v1.PATCH("/todos/:id", todoPATCH)
		v1.DELETE("/todos/:id", todoDELETE)
	}
	router.Run(":8080")
}
// ...　省略
```
```v1.Use(authMiddleware())```とするとv1のパスへのリクエストにJWT認証機能を付けることができます。
### HTTPリクエストにJWTを追加する
まだこの段階ではサーバー側へJWTを送っていないので、クライアント側でログインしていたとしてもAPIは叩けません。
HTTPリクエストのHeaderにJWTを入れてサーバーに送る処理を追加しましょう。
```front/src/components/TodoList.js
// ... 省略
const TodoList = () => {
  const [todo, setTodo] = useState('');
  const [todos, setTodos] = useState([]);

  const handleChangeTodo = (event) => {
    setTodo(event.target.value);
  }

  const getTodosData = async () => {
    await axios.get("http://localhost:8080/todo/api/v1/todos", {
      headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
    })
      .then((res) => {
        setTodos(res.data);
      }).catch((err) => {
        console.error(err);
      })
  }

  useEffect(() => {
    getTodosData();
  }, []);

  const handleCreateTodo = async () => {
    const params = new URLSearchParams;
    params.append("title", todo);
    await axios.post("http://localhost:8080/todo/api/v1/todos", params, {
      headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
    }).then(() => {
        getTodosData();
        setTodo('');
      })
      .catch((err) => {
        console.error(err);
      });
    console.log("create todo");
  }

  const handleDeleteTodo = async (id) => {
    await axios.delete(`http://localhost:8080/todo/api/v1/todos/${id}`, {
      headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}`}
    }).then(() => {
        getTodosData();
      })
      .catch((err) => {
        console.error(err);
      });
  }
// ... 省略
```
```getTodosData```,```handleCreateTodo```,```handleDeleteTodo```のaxiosに```headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}`}```を追加します。サインイン時にローカルストレージに保存したJWTをサーバーに送ることで認証できます。

これで一通りJWT認証機能は完成しました。[Restlet Client](https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ja)を使って試してみましょう。
Restlet ClientからAPIを叩いた場合、リクエストにJWTを含んでいないためステータスコード401が返ってくるはずです。

![apiTest.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1726236/f111fe30-e1bd-ea6d-5a72-3f6da9e2f74c.jpeg)
Reactで作ったUIからログインするとtodoの表示、作成、削除を一通り行えると思います。

いろいろ改善点はあると思いますが、とりあえずJWT認証つきの簡単なTODOアプリケーションは完成です！
今回はここまでです。ありがとうございました！


### 参考記事

* [Go言語で理解するJWT認証 実装ハンズオン](https://qiita.com/po3rin/items/740445d21487dfcb5d9f)
* [nginx + golangでWeb API開発](https://qiita.com/unokun/items/dcf3ed4fbc4f532f664a)
* [0からREST APIについて調べてみた](https://qiita.com/masato44gm/items/dffb8281536ad321fb08)
* [Docker MySQLコンテナ起動時に初期データを投入する](https://qiita.com/NagaokaKenichi/items/ae037963b33a85df33f5)
* [GoでMySQL接続するメモ](https://qiita.com/rmanzoku/items/e2a1d50c58dedba3f5a9)
* [なんとなく CORS がわかる...はもう終わりにする。](https://qiita.com/att55/items/2154a8aad8bf1409db2b)
* [【完全版】ReactのFirebase Authentication(認証)を基礎からマスターする](https://reffect.co.jp/react/react-firebase-auth)
* [GolangのWebフレームワークginのmiddlewareについての覚書](https://qiita.com/tobita0000/items/d2309cc3f0dd32006ead)
* [Docker 開発環境の中から GCP サービスへの認証を通すかんたんなやり方](https://qiita.com/castaneai/items/1a5c02c3031ef394b33e)