はじめに
こんにちはRIN1208です。
この記事はタイトルに書いてある通り、完全に個人で使う用に作成したのでそれについて書いていきたいと思います。
今回作ったものはこちら
この記事はITRCアドベントカレンダーの1日目の記事になります。
今回は上記のような構成で作成しています。サーバーサイドはHeroku、フロントエンドはFirebase Hosingにデプロイしています。
またJWTが無効な値だった場合は6, 7の部分の処理はせずにエラーを返すようにしました。
できたもの
以下のようなURLを貼り付けて管理するだけのサービスです。完全個人用なので規約等はありません。
シンプルなのもにしたかったので機能は特にありません。
使用技術
## 環境
- 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の取得の部分です
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のプロジェクを開き
上の画像のように プロジェクトの設定 > サービスアカウント > 新しい秘密鍵の生成 をタップし生成して下さい。このjsonファイルはGitHubに上げたりしないで下さい。
CORSの設定
以下のように書きCORSの設定をします
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っぽく書いています。
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を作成するようにしました
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形式のファイルであれば問題ないです。
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を使用しましたがめっちゃ便利でした。ただ個人用ですのでエラーハンドリングやログもかなり適当になってます......
また間違っている点などがございましたらコメントなどで指摘していただけると助かります。