完全個人用のURL管理サービスをReact×Firebase×Go×Herokuで作った話

# はじめに
こんにちはRIN1208です。

この記事はタイトルに書いてある通り、完全に個人で使う用に作成したのでそれについて書いていきたいと思います。
今回作ったものは[こちら](https://github.com/rin1208/link-note)



この記事は[ITRCアドベントカレンダー](https://adventar.org/calendars/5535)の1日目の記事になります。

#　今回の構成
![スクリーンショット 2020-11-25 14.17.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329144/baf9e69f-b692-f375-624e-e4479579a34e.png)


今回は上記のような構成で作成しています。サーバーサイドはHeroku、フロントエンドはFirebase Hosingにデプロイしています。
またJWTが無効な値だった場合は6, 7の部分の処理はせずにエラーを返すようにしました。

## できたもの
以下のようなURLを貼り付けて管理するだけのサービスです。完全個人用なので規約等はありません。
シンプルなのもにしたかったので機能は特にありません。
![スクリーンショット 2020-12-01 16.15.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329144/07709750-474b-ea69-d51c-aae2723ac34e.png)



# 使用技術

##　環境
- golang 
- react(yarn)
- firebase cli
- heroku cli

上記の環境がある前提で説明していきます

### フロントエンド

- React.js  
reduxは使用しておりません
- axios
- material-ui  
cssが大の苦手な為使用しました
- Firebase  
フロントエンドは認証と Firebase Hosingを使用しました

### バックエンド
- Golang (Gin)
- Heroku
- FireStore  
フロントで使用しても良かったのですがサーバーサイド書きたかったのでしようしました。

### CI

- GitHub Actions  
masterにpushされた際にフロントはFirebase Hosing、バックエンドはHerokuにデプロイれるようにしました


#この記事で説明する部分
今回作ったものを説明するにあたり、全て説明するとさすがに長くなるので以下の要所のみ説明していきます
###フロントエンド
- JWTの部分
 
###サーバーサイド
- JWTの部分
- FireStoreの部分
- GitHub Actionsについて




#フロントエンド
まずはフロントエンドjwtの取得の部分です

```js
	const [loading, setLoading] = useState(true);
	const [user, setUser] = useState(null);

	useEffect(() => {
		firebase.auth().onAuthStateChanged(user => {
			setLoading(false)
			setUser(user)
			if (user) {
				user.getIdToken().then(function (idToken) {
					localStorage.setItem('jwt', idToken)
				});
				localStorage.setItem('uid', user.uid)
			}
		})
	})
	const logout = () => {
		firebase.auth().signOut();
		localStorage.removeItem('uid')
	}
```
ログイン時にuser.getIdToken().then(function (idToken) でjwtを取得し、
localStorage.setItem('jwt', idToken)でローカルストレージにjwtを保存しています


#バックエンド
## GoでFirestoreを扱うための下準備
FireBaseのプロジェクを開き![スクリーンショット 2020-12-01 16.42.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329144/29e33022-3601-35a5-7ca0-c78565b4a04a.png)
上の画像のように プロジェクトの設定 > サービスアカウント > 新しい秘密鍵の生成 をタップし生成して下さい。このjsonファイルはGitHubに上げたりしないで下さい。

## CORSの設定
以下のように書きCORSの設定をします

```go
	port := os.Getenv("PORT")
	if port == "" {
		err := godotenv.Load(fmt.Sprintf("./%s.env", os.Getenv("GO_ENV")))
		if err != nil {
			fmt.Println(err)
		}
		port = os.Getenv("LOCAL_PORT")
	}
	r := gin.Default()
	r.Use(cors.New(cors.Config{
		AllowMethods: []string{
			"POST",
			"GET",
			"OPTIONS",
			"PUT",
			"DELETE",
		},
		AllowHeaders: []string{
			"Content-Type",
			"Content-Length",
			"Authorization",
			"Uid",
		},
		AllowOrigins: []string{
			"http://localhost:3000",
			os.Getenv("FRONT_URL1"),
			os.Getenv("FRONT_URL2"),
		},
		MaxAge: 24 * time.Hour,
	}))

	pkg.Serve(r, ":"+port)

```
##Firestoreの処理

FireStoreの処理の部分です。interfaceを使用してClean Architectureっぽく書いています。  


```go
type FirestoreAuth struct {
	Type                        string `json:"type"`
	Project_id                  string `json:"project_id"`
	Private_key_id              string `json:"private_key_id"`
	Private_key                 string `json:"private_key"`
	Client_email                string `json:"client_email"`
	Client_id                   string `json:"client_id"`
	Auth_uri                    string `json:"auth_uri"`
	Token_uri                   string `json:"token_uri"`
	Auth_provider_x509_cert_url string `json:"auth_provider_x509_cert_url"`
	Client_x509_cert_url        string `json:"client_x509_cert_url"`
}

type FireBaseClient struct {
	FireBase      *firebase.App
	FireStore     *firestore.Client
	Ctx           context.Context
	CollectionRef *firestore.CollectionRef
	DocumentRef   *firestore.DocumentRef
	Auth          *auth.Client
}

type FireBaseHandler interface {
	Collection(path string) *FireBaseClient
	Set(ctx context.Context, data interface{}) error
	Doc(id string) *FireBaseClient
	Documents(ctx context.Context) *firestore.DocumentIterator
	Delete(ctx context.Context) error
	VerifyIDToken(ctx context.Context, idToken string) error
}

type FireBase struct {
	FireBaseHandler
}

func Init_firebase() FireBaseHandler {

	ctx := context.Background()
	sa := option.WithCredentialsFile("./firestore.json")
	app, err := firebase.NewApp(ctx, nil, sa)
	if err != nil {
		return nil
	}
	client, err := app.Firestore(ctx)

	if err != nil {
		return nil
	}
	auth, err := app.Auth(ctx)
	if err != nil {
		return nil

	}

	return &FireBaseClient{
		FireBase:  app,
		FireStore: client,
		Ctx:       ctx,
		Auth:      auth,
	}
}
//データを書き込み
func (fb *FireBase) InsertData(data model.Content) {

	updateError := fb.Collection(data.Uid).Doc(data.Content_id).Set(context.Background(), map[string]interface{}{
		"content_id": data.Content_id,
		"comment":    data.Comment,
		"url":        data.Url,
		"date":       data.Date,
	})
	if updateError != nil {
		log.Printf("An error has occurred: %s", updateError)
	}
}

//データを削除
func (fb *FireBase) DeleteData(uid, id string) error {

	err := fb.Collection(uid).Doc(id).Delete(context.Background())

	if err != nil {
		return err
	}
	return nil
}
//データを取得
func (fb *FireBase) GetData(uid string) []model.Content {

	var res_data []model.Content
	iter := fb.Collection(uid).Documents(context.Background())

	for {
		doc, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			log.Fatalf("Failed to iterate: %v", err)
		}
		data := doc.Data()
		var content model.Content
		content.Comment = data["comment"].(string)
		content.Url = data["url"].(string)
		content.Content_id = data["content_id"].(string)
		content.Date = int(data["date"].(int64))

		res_data = append(res_data, content)
	}

	return res_data

}
//jwt認証
func (fb *FireBase) AuthJWT(jwt string) error {

	idToken := strings.Replace(jwt, "Bearer ", "", 1)
	err := fb.VerifyIDToken(context.Background(), idToken)
	if err != nil {
		return err
	}
	return nil
}

func (fb *FireBaseClient) VerifyIDToken(ctx context.Context, idToken string) error {
	_, err := fb.Auth.VerifyIDToken(ctx, idToken)
	return err
}

func (fb *FireBaseClient) Collection(path string) *FireBaseClient {
	fb.CollectionRef = fb.FireStore.Collection(path)
	return fb
}

func (fb *FireBaseClient) Set(ctx context.Context, data interface{}) error {
	_, err := fb.DocumentRef.Set(ctx, data, firestore.MergeAll)
	return err
}

func (fb *FireBaseClient) Doc(id string) *FireBaseClient {
	fb.DocumentRef = fb.CollectionRef.Doc(id)
	return fb
}
func (fb *FireBaseClient) Documents(ctx context.Context) *firestore.DocumentIterator {
	res := fb.CollectionRef.Documents(ctx)
	return res
}

func (fb *FireBaseClient) Delete(ctx context.Context) error {
	_, err := fb.DocumentRef.Delete(ctx)
	return err
}
```
GoでFireStoreをを使用する際にFireBaseの認証のjsonを読み込ませるのですがgithubにpushするわけにも行かないので今回はjsonを作成するようにしました


```go

func CreateFireStoreJson() {
	fp, err := os.Create("./firestore.json")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer fp.Close()

	file := fmt.Sprintf(` {
	"type": "%s",
	"project_id": "%s",
	"private_key_id": "%s",
	"private_key": "%s",
	"client_email": "%s",
	"client_id": "%s",
	"auth_uri": "%s",
	"token_uri": "%s",
	"auth_provider_x509_cert_url": "%s",
	"client_x509_cert_url": "%s"
}`,
		os.Getenv("FS_TYPE"),
		os.Getenv("FS_PROJECT_ID"),
		os.Getenv("FS_PRIVATE_KEY_ID"),
		os.Getenv("FS_PRIVATE_KEY"),
		os.Getenv("FS_CLIENT_EMAIL"),
		os.Getenv("FS_CLIENT_ID"),
		os.Getenv("FS_AUTH_URI"),
		os.Getenv("FS_TOKEN_URI"),
		os.Getenv("FS_AUTH_PROVIDER_X509_CERT_URL"),
		os.Getenv("FS_AUTH_PROVIDER_X509_CERT_URL"))

	_, err = fp.Write(([]byte)(file))
	if err != nil {
		fmt.Println(err)
	}
}
```


# GitHub Actionsを使ってFirebaseとHerokuにデプロイする

## ymlを作成する

.github/workflows/deploy.ymlをプロジェクトのルートディレクトリに作成して下さい。
これはGithub Actionsの設定ファイルです。今回はdeploy.ymlにしていますが.yml形式のファイルであれば問題ないです。

```yml
name: ci

on:
  push:
    braches: 
      - master

jobs:
  firebase:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup node
        uses: actions/setup-node@v1
        with:
          node-version: 14.4.0

      - name: Install dependencies
        run: |
          yarn
      - name: Build React app
        env:  #FireBaseの環境変数を定義しています
          REACT_APP_FB_API_KEY: ${{ secrets.REACT_APP_FB_API_KEY }}
          REACT_APP_FB_AUTH_DOMAIN: ${{ secrets.REACT_APP_FB_AUTH_DOMAIN }}
          REACT_APP_FB_DATABASE_URL: ${{ secrets.REACT_APP_FB_DATABASE_URL }}
          REACT_APP_FB_PROJECT_ID: ${{ secrets.REACT_APP_FB_PROJECT_ID }}
          REACT_APP_FB_STORAGE_BUCKET: ${{ secrets.REACT_APP_FB_STORAGE_BUCKET }}
          REACT_APP_FB_MESSAGEING_SENDER_ID: ${{ secrets.REACT_APP_FB_MESSAGEING_SENDER_ID }}
          REACT_APP_SERVER_URL: ${{ secrets.REACT_APP_SERVER_URL }}
        run: |
          yarn install && yarn build
      - name: Setup Firebase CLI
        run: |
          npm install -g firebase-tools
      - name: Deploy Firebase
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}  #firebaseのcitoken
        run: |
          firebase deploy --token $FIREBASE_TOKEN
  backend:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
      with:
          fetch-depth: 0
    - name: Deploy to Heroku
      env:
        HEROKU_API_TOKEN: ${{ secrets.HEROKU_API_TOKEN }} # herokuのcitoken
        HEROKU_APP_NAME: herokuのプロジェクト名
      if: github.ref == 'refs/heads/master' && job.status == 'success'
      run: |
        git push https://heroku:$HEROKU_API_TOKEN@git.heroku.com/$HEROKU_APP_NAME.git origin/master:master

```


## CIトークンを取得する
###FireBaseのトークンを取得する
```
firebase login:ci
```
上記のコマンドを打つと以下のようにトークンが表示されます

```
✔  Success! Use this token to login on a CI server:

{TOKENの文字列}
```

###Herokuのトークンを取得する
```
heroku auth:token
```
上記のコマンドを打つと以下のようにトークンが表示されます

```
 ›   Use heroku authorizations:create to generate a long-term token
{TOKENの文字列}
```
GitHub ActionsでHerokuにデプロイする際にProfile等は必要ないみたいです

##環境変数を定義する
リポジトリを開き Setting > Secrets　>　New repository secret で環境変数を設定します
設定する際は${{ secrets.FIREBASE_TOKEN }}のような書き方で取得できます。

上記のが完了したらmasterにpushするとFireBaseとHerokuにデプロイされるようになります。




#おわりに
ここまで読んでくださりありがとうございます。
今回は個人で使用するURLを管理するサービスについて書きました。
初めてGitHub Actionsを使用しましたがめっちゃ便利でした。ただ個人用ですのでエラーハンドリングやログもかなり適当になってます......

また間違っている点などがございましたらコメントなどで指摘していただけると助かります。