【実践】SPA+Firebaseで爆速で作るウェブアプリケーション開発

備忘録も兼ねて最速で環境構築して開発してデプロイ
[Homebrew](https://brew.sh/index_ja), [yarn](https://yarnpkg.com/lang/en/docs/install/#mac-stable), [nodebrew](https://github.com/hokaccha/nodebrew), [parcel](https://github.com/parcel-bundler/parcel)などの導入は略

# 構成
モダンなJS環境の構築を最速でやるにはparcelを使う。
webpackのような煩雑な設定が不要、細かい設定はwebpackでないとできないこともあるが
大体の場合、裏側で上手いこといい感じにbabelのES2015/2017トランスパイルもしてくれる。

・[GitHub環境構築テンプレート](https://github.com/teradonburi/firebase-template)

・Parcel
・SPA(React、Vue)
・Firebase

多分これが一番はやいと思います。

## Parcelのリソースパスの指定
画像などのアセットファイルパスはビルド時にファイル名が変換されてしまうため、require指定でパスを指定する必要があります。

```
<img src={require('../img/logo.png')} width={100} height={100} />
```

## eslint
文法チェック、最近はないと逆に開発速度が落ちる。
未使用の変数とか、非推奨な文法とかのチェックもできる。
VSCodeの場合、エラーは赤字、warningは緑字の波線で表示されて可視化されるので把握しやすい。
また、設定のeslint.autoFixOnSaveをtrueにしておくとファイル保存時に`eslint --fix`を走らせることができて便利。

```settings.json
{
    "eslint.autoFixOnSave": true
}
```

eslint系のプラグインの導入

```.sh
$ yarn add --dev eslint babel-eslint eslint-loader eslint-plugin-react
```

個人的によく使うlint設定(.eslintrc.js)

```.eslintrc.js
module.exports = {
  'parser': 'babel-eslint',
  'env': {
    'browser': true, // ブラウザ
    'es6': true, // ES6
    'node': true, // NodeJS
  },
  // reactプラグイン
  'extends': ['eslint:recommended', 'plugin:react/recommended'],
  'parserOptions': {
    'ecmaFeatures': {
      'experimentalObjectRestSpread': true,
      'jsx': true, // JSX文法有効
      'legacyDecorators': true
    },
    'sourceType': 'module'
  },
  'settings': { 
    'react': { 'version' : '16.6.3' }
  },
  // reactプラグイン使用
  'plugins': [
    'react'
  ],
  'globals': {
  },
  'rules': {
    // インデントルール
    'indent': [
      'error',
      2,
      { 'SwitchCase': 1 }
    ],
    // 改行コード
    'linebreak-style': [
      'error',
      'unix'
    ],
    // シングルクォートチェック
    'quotes': [
      'error',
      'single'
    ],
    // 末尾セミコロンチェック
    'semi': [
      'error',
      'never'
    ],
    // マルチライン末尾コンマ必須
    'comma-dangle': [
      'error',
      'always-multiline'
    ],
    // 末尾スペースチェック
    'no-trailing-spaces': [
      'error'
    ],
    // 単語間スペースチェック
    'keyword-spacing': [
      'error',
      { 'before': true, 'after': true }
    ],
    // オブジェクトコロンのスペースチェック
    'key-spacing': [
      'error',
      { 'mode': 'minimum' }
    ],
    // コンマ後スペースチェック
    'comma-spacing': [
      'error',
      { 'before': false, 'after': true }
    ],
    // ブロック前スペースチェック
    'space-before-blocks': [
      'error'
    ],
    // アロー関数スペースチェック
    'arrow-spacing': [
      'error',
      { "before": true, "after": true }
    ],
    // 括弧内のスペースチェック
    'space-in-parens': [
      'error',
      'never'
    ],
    // オブジェクトのdot記法強制
    'dot-notation': [
      'error'
    ],
    // ブロックを不要に改行しない
    'brace-style': [
      'error',
      '1tbs'
    ],
    // elseでreturnさせない
    'no-else-return': [
      'error'
    ],
    // 未使用変数チェック
    'no-unused-vars': [
      'warn',
      { 'ignoreRestSiblings': true }
    ],
    'no-console': 'off',
    // reactのprop-typesチェックをしない
    'react/prop-types': 'off',
    // reactのコンポーネント名チェックをしない
    'react/display-name': 'off',
    // stateless functional componentを優先させる
    'react/prefer-stateless-function': [
      2,
      { 'ignorePureComponents': true }, // PureComponentsは除く
    ],
    // 静的クラスのプロパティとライフサイクルメソッドを宣言する際に、大文字と小文字の区別がないようにする
    'react/no-typos': 'error',
    // 未使用propsはエラー
    'react/no-unused-prop-types': 'error',
    // 未使用stateはエラー
    'react/no-unused-state': 'error',
    // 中身が空のタグはself closingをさせる
    'react/self-closing-comp': 'error',
  }
}
```

## SPA
個人的にReactが好きなのでReactを使う。（別にvueとかでも良い）

```.sh
$ yarn add --dev react react-dom react-hot-loader
```

bebelトランスパイルプラグインで追加の文法を変換

```.sh
$ yarn add --dev　@babel/core　@babel/plugin-proposal-class-properties　@babel/plugin-proposal-decorators　@babel/plugin-syntax-dynamic-import　@babel/preset-env　@babel/preset-react
```

.babelrcにプラグイン追加

```.babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }], 
    ["@babel/plugin-proposal-class-properties", { "loose": true }], 
    ["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }],
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-syntax-dynamic-import",
  ]
}
```

## アプリケーション状態管理
Redux、React-Redux、非同期処理

```.sh
$ yarn add --dev redux react-redux　redux-thunk axios
```

## ページ遷移
react-router系

```.sh
$ yarn add --dev react-router react-router-dom　connected-react-router
```

## UI&フォーム
マテリアルUIのライブラリ導入(MUI)

```
$ yarn add --dev @material-ui/core @material-ui/icons
```

redux-formでページのフォーム入力情報をreduxで管理

```
$ yarn add --dev redux-form
```

# バックエンドとインフラ
Firebaseに必要最低限なものが全部揃っている
[カスタムドメインの設定も可能](https://qiita.com/nkhrk/items/6706f27207f694049423)

- Authentication: 認証
- Database(RealTime Database, Cloud Firestore): データベース
- Storage: ファイルストレージ
- Function: Rest API
- Hosting: publicフォルダ（SPA）

ドキュメント：
[Firebase のガイド](https://firebase.google.com/docs/guides/?hl=ja)
[Firebase API Reference](https://firebase.google.com/docs/reference/?hl=ja)
[Cloud Functions for Firebase Sample Library](https://github.com/firebase/functions-samples)

firebase-tools導入後、アカウントのアクティベートのため、ローカルからログインする

```.sh
$ firebase login
```

[Cloud Functionsのローカルエミュレータのダウンロード](https://cloud.google.com/functions/docs/emulator?hl=ja)
Node v8に対応したいのでオプションでエラー回避

```
$ yarn global add @google-cloud/functions-emulator --ignore-engines
```

開発用と本番用でDBを２つ以上作りたくなるが、１つのプロジェクトで複数DBは[課金](https://firebase.google.com/pricing/?authuser=0)が必要・・・

StorageのCORS設定ファイルをアップロードするために必要（ファイルダウンロードの実装に必要になる）
[gsutil をインストールする](https://cloud.google.com/storage/docs/gsutil_install?hl=ja)

```.sh
$ curl https://sdk.cloud.google.com | bash
$ source ~/.bash_profile
$ gcloud init
```

# 実践
プロジェクトをtemplateフォルダに作成すると仮定する

```.sh
$ mkdir template && cd test
$ touch README.md
```

package.jsonの作成

```.sh
$ yarn init -y
```

parcel導入、eslint導入、React導入、Redux導入、React Router導入
babel系文法プラグイン導入

```.sh
$ yarn add --dev parcel-bundler eslint babel-eslint eslint-loader eslint-plugin-react react react-dom react-hot-loader @babel/core　@babel/plugin-proposal-class-properties　@babel/plugin-proposal-decorators　@babel/plugin-syntax-dynamic-import　@babel/preset-env　@babel/preset-react redux react-redux react-router react-router-dom connected-react-router @material-ui/core @material-ui/icons redux-form
```

Firebase導入

```.sh
$ yarn add firebase
```

scriptsに
parcelの起動コマンドの`"start": "parcel src/index.html -d public"`と
を追加

package.jsonはこんな感じ

```package.json
{
  "name": "test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "UNLICENSED",
  "scripts": {
    "start": "parcel src/index.html -d public",
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-proposal-class-properties": "^7.2.1",
    "@babel/plugin-proposal-decorators": "^7.2.2",
    "@babel/plugin-proposal-optional-chaining": "^7.2.0",
    "@babel/plugin-proposal-pipeline-operator": "^7.2.0",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/preset-env": "^7.2.0",
    "@babel/preset-react": "^7.0.0",
    "@material-ui/core": "^3.6.2",
    "@material-ui/icons": "^3.0.1",
    "axios": "^0.18.0",
    "babel-eslint": "^10.0.1",
    "connected-react-router": "^6.0.0",
    "eslint": "^5.10.0",
    "eslint-loader": "^2.1.1",
    "eslint-plugin-react": "^7.11.1",
    "parcel-bundler": "^1.10.3",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "react-hot-loader": "^4.6.0",
    "react-redux": "^6.0.0",
    "react-router": "^4.3.1",
    "react-router-dom": "^4.3.1",
    "redux": "^4.0.1",
    "redux-form": "^8.0.4",
    "redux-thunk": "^2.3.0"
  },
  "dependencies": {
    "firebase": "^5.7.0"
  }
}
```

プロジェクトのフォルダ構成

```
├── .babelrc
├── .eslintrc
├── .gitignore
├── README.md
├── package.json
├── public
├── src
│   ├── index.html
│   └── js
│       └── index.js
└── yarn.lock
```

index.htmlテンプレ

```index.html
<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>テンプレート</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 </head>
 <body>
  <div id="root"></div>
  <script src="js/index.js"></script>
 </body>
</html>
```

index.js、とりあえず最低限のセットアップ
・Reduxストアの作成
・ページ遷移
・MUIの初期化
・React Hot Loaderの設定

```index.js
/*globals module: false process: false */
import React  from 'react'
import ReactDOM from 'react-dom'
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import { Provider } from 'react-redux'
import createHistory from 'history/createBrowserHistory'
import thunk from 'redux-thunk'
import axios from 'axios'
import { reducer as formReducer } from 'redux-form'
import { Route, Switch } from 'react-router-dom'
import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-react-router'
import { MuiThemeProvider } from '@material-ui/core/styles'
import { createMuiTheme } from '@material-ui/core/styles'

// mui theme
const theme = createMuiTheme({
  typography: {
    useNextVariants: true,
  }}
)

const client = axios.create()

// redux-devtoolの設定
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
// ブラウザ履歴保存用のストレージを作成
const history = createHistory()
// axiosをthunkの追加引数に加える
const thunkWithClient = thunk.withExtraArgument(client)

// reducer
const reducer = combineReducers({
  router: connectRouter(history),
  form: formReducer,
})

// redux-thunkをミドルウェアに適用、historyをミドルウェアに追加
const store = createStore(connectRouter(history)(reducer), {}, composeEnhancers(applyMiddleware(routerMiddleware(history), thunkWithClient)))

const TopPage = () => <div>Hello World!!</div>
const NotFound = () => <div>Not found</div>


const App = ({history}) => (
  <ConnectedRouter history={history}>
    <Switch>
      <Route exact path='/' component={TopPage} />
      <Route component={NotFound} />
    </Switch>
  </ConnectedRouter>
)

ReactDOM.render(
  <MuiThemeProvider theme={theme}>
    <Provider store={store}>
      <App history={history} />
    </Provider>
  </MuiThemeProvider>,
  document.getElementById('root'),
)
```

firebaseのプロジェクトを作成する

```
$ firebase init

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/daikiterai/Desktop/private/test

? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, the
n Enter to confirm your choices. (Press <space> to select)
❯◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◯ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
```

使いたいサービスにスペースキーでチェックを入れる
・Database：RealTime Database
・Functions：API
・Hosting：SPAホスティング
・Storage:ファイルサービス
にチェックを入れる

以下のfirebase関連のファイルが作成されます。

- functionsフォルダ
- database.rules.json
- storage.rules
- firebase.json


async/awaitを使いたいので
functions/package.jsonにNode v8の設定を追加（デフォルトだとNode v6になっている）

```functions/package.json
  "engines": { "node": "8" },
```

functions/package.jsonにexpress関連のパッケージを追加

```
$ yarn add express cors
```

functions/package.jsonはこんな感じ

```functions/package.json
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "serve": "firebase serve --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.16.4",
    "firebase-admin": "~6.0.0",
    "firebase-functions": "^2.1.0"
  },
  "engines": {
    "node": "8"
  },
  "private": true
}
```

バックエンドの処理をfunctions/index.jsに書きます。
expressでAPIのルーティングと認証をさせることができます。

```functions/index.js
'use strict'

const functions = require('firebase-functions')
const admin = require('firebase-admin')
const serviceAccount = require('admin認証情報.json')
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: `https://${serviceAccount.project_id}.firebaseio.com/`,
  storageBucket: `${serviceAccount.project_id}.appspot.com`,
})
const database = admin.database()
const storage = admin.storage()
const bucket = storage.bucket()

const express = require('express')
const cors = require('cors')({origin: true})
const app = express()

// JWTトークンの検証を行う（認証）
const validate = async (req, res, next) => {

  if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
      !(req.cookies && req.cookies.__session)) {
    return res.status(403).send('Unauthorized')
  }

  let idToken
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
    // ID Tokenをthe Authorization headerから取得
    idToken = req.headers.authorization.split('Bearer ')[1]
  } else {
    // 認証失敗
    return res.status(403).send('Unauthorized')
  }

  try {
    const decodedIdToken = await admin.auth().verifyIdToken(idToken)
    console.log('ID Token correctly decoded', decodedIdToken)
    // ID Token decodedをreq.userに格納
    req.user = decodedIdToken
    return next()
  } catch (error) {
    console.error('Error while verifying Firebase ID token:', error)
    return res.status(403).send('Unauthorized')
  }
}

app.use(cors)

async function ping(req, res) {
  res.send('ok')
}

async function info(req, res) {
  const id = req.user.uid
  const msg = req.body.msg
  if (!msg) return res.status(400).json()
  // pathで格納される
  await database.ref(`/blog/${id}`).set(msg)
  // path以下の子がすべて取れる
  const result = await database.ref(`/blog/${id}`).once('value').then((snapshot) => snapshot.val())
  res.json(result)
}

async function addFilePermission(req, res) {
  const path = req.body.path
  const fileRef = bucket.file(path)
  let result = await fileRef.getMetadata()
  console.log(result)
  const data = {
    metadata: {},
  }
  data.metadata[req.user.uid] = true
  result = await fileRef.setMetadata(data)
  console.log(result)

  res.json({message: 'success'})
}

// 認証なしAPI
app.get('/', ping)

app.use(
  '/user',
  express.Router()
    // ユーザ認証付きAPI
    // `Authorization` HTTP headerに`Bearer <Firebase ID Token>`が必須
    .post('/info', validate, info)
    .post('/permission', validate, addFilePermission)
)

// それ以外
app.all('*', (req, res) => {
  res.status(404).send('not found')
})

exports.api = functions.https.onRequest(app)
```

admin認証情報.jsonの払い出しはプロジェクトの設定→サービスアカウントより生成できます。
このjsonファイルの設定値はあとで説明するアクセスルール関係なしにDBにアクセスできるため、<font color='red'>Gitなどのリポジトリにはアップロードしてはいけません</font>
![スクリーンショット 2018-12-21 0.52.39.png](https://qiita-image-store.s3.amazonaws.com/0/55077/544aec72-d4e9-fac5-502b-3f5a5bd21f3c.png)

DBのURLはDatabaseの画面から確認できます。
![スクリーンショット 2018-12-21 0.51.46.png](https://qiita-image-store.s3.amazonaws.com/0/55077/f1366d93-6977-74a9-3af3-ab4263620d76.png)

StorageのURLはStorage画面から確認できます
![スクリーンショット 2018-12-21 0.51.25.png](https://qiita-image-store.s3.amazonaws.com/0/55077/4d66cc1d-22b5-c7d5-678c-6e8b756539f4.png)


プロジェクトの設定→ウェブアプリにFirebaseを追加を開く
<img width="1299" alt="スクリーンショット 2018-12-19 9.12.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/3f09d75f-9508-2e5e-edf6-bb6c2c9eb6ac.png">

configオブジェクトのプロジェクト情報はアプリ単位に違うものを設定します。
![スクリーンショット 2018-12-19 9.12.47.png](https://qiita-image-store.s3.amazonaws.com/0/55077/045b2083-2cfa-32d5-939c-8dbbf6218f50.png)

以下、クライアントサイドでは`src/lib/firebase.js`にてfirebaseライブラリの初期化を行います。

```src/lib/firebase.js
import firebase from 'firebase'

// プロジェクト別に違う
const config = {
  apiKey: '<API Key>',
  authDomain: '<アプリのドメイン>',
  databaseURL: '<データベースURL>',
  projectId: '<プロジェクトID>',
  storageBucket: '<StorageURL>',
  messagingSenderId: '<Push通知用ID>',
}
firebase.initializeApp(config)
```

## Functionsで認証付きのAPIを作る
クライアントサイドでauthライブラリを使うことでログインをすることができます。
この際、払い出されるJWTトークンをバックエンド側に送信し、バックエンド側で正当なJWTトークンか検証することで認証付きAPIの実装を行うことができます。

```src/lib/firebase.js
firebase.initializeApp(config)
const auth = firebase.auth()

// apiのエンドポイント
export const apiEndpoint = document.location.hostname === 'localhost' ?
  `http://localhost:5001/${config.projectId}/us-central1` :
  `https://us-central1-${config.projectId}.cloudfunctions.net`


export function signIn() {
  // Googleログイン
  auth.signInWithPopup(new firebase.auth.GoogleAuthProvider())
}

export function signOut() {
  auth.signOut()
}

export function authChange(callback) {
  auth.onAuthStateChanged((user) => {
    if (user) {
      // ログイン時にAuthorization Headerにtokenを付与
      firebase.auth().currentUser.getIdToken().then((token) => {
        callback({uid: user.uid, token})
      })
    } else {
      // ログアウト
      callback({uid: null, token: null})
    }
  })
}

```

ログイン方法はメジャーなものはauthライブラリ側で実装されています。
今回の例ではAuthentication→ログイン方法でGoogle OAuth認証を有効にします。

<img width="1241" alt="スクリーンショット 2018-12-21 1.11.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/200c5513-239f-a811-3ebe-9e6822b4091c.png">

クライアントサイドで認証を行い、tokenをreduxに保存します。
以降、functionsのAPIをコールする際にtokenを付与するようにします。

```src/index.js
import { connect } from 'react-redux'
import { signIn, signOut, authChange, apiEndpoint } from './lib/firebase'

const LOGIN = 'user/LOGIN'
const LOGOUT = 'user/LOGOUT'
const INFO = 'user/INFO'

function userReducer(state = {uid: null, token: null, info: ''}, action = {}) {
  switch (action.type) {
    case LOGIN:
      return {
        ...state,
        uid: action.uid,
        token: action.token,
      }
    case LOGOUT:
      return {
        ...state,
        uid: null,
        token: null,
      }
    case INFO:
      return {
        ...state,
        info: action.info,
      }
    default:
      return state
  }
}

// reducer
const reducer = combineReducers({
  router: connectRouter(history),
  form: formReducer,
  userReducer,
})

// 認証状態の変更があった場合に呼ばれる（ログイン、ログアウト）
authChange(({uid, token}) => {
  if (token) {
    store.dispatch({type: LOGIN, uid, token})
  } else {
    store.dispatch({type: LOGOUT})
  }
})

client.interceptors.request.use(req => {
  const token = store.getState().userReducer.token

  // ログイン後はtokenを使ってリクエスト
  if (token) {
    // ieのリクエストキャッシュ対策
    document.execCommand && document.execCommand('ClearAuthenticationCache', 'false')
    req.url += (req.url.indexOf('?') == -1 ? '?' : '&') + '_=' + Date.now()
    // ユーザ認証トークン付与
    req.headers.Authorization = `Bearer ${token}`
  }
  return req
}, err => Promise.reject(err))

client.interceptors.response.use(res => res, err => {
  if (axios.isCancel(err)) {
    return Promise.reject({code: 999, message: 'cancel'})
  }
  return Promise.reject(err.response || {})
})

function sendInfo(msg) {
  return (dispatch, getState, client) => {
    return client
      .post(apiEndpoint + '/api/user/info', msg)
      .then(res => res.data)
      .then(info => {
        dispatch({type: INFO, info})
        return info
      })
  }
}

const TopPage = connect(
  state => ({
    uid: state.userReducer.uid,
    info: state.userReducer.info,
  }),
  { sendInfo }
)(({uid, info, chats, sendInfo, sendChat, getFilePermission}) => (
  <div>
    {uid === null ?
      <button onClick={() => signIn()}>Sign in with Google</button> :
      <button onClick={() => signOut()}>Sign out</button>
    }
    {uid && <button onClick={() => sendInfo({msg: 'hoge'})}>info送信</button>}
  </div>
))
```

functions側で認証付きAPIはJWTトークンの検証を行います。
認証したいAPIに関しては、validate関数を手前に挟むことでログイン認証をかけることができます。

```functions/index.js
// JWTトークンの検証を行う（認証）
const validate = async (req, res, next) => {

  if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
      !(req.cookies && req.cookies.__session)) {
    return res.status(403).send('Unauthorized')
  }

  let idToken
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
    // ID Tokenをthe Authorization headerから取得
    idToken = req.headers.authorization.split('Bearer ')[1]
  } else {
    // 認証失敗
    return res.status(403).send('Unauthorized')
  }

  try {
    const decodedIdToken = await admin.auth().verifyIdToken(idToken)
    console.log('ID Token correctly decoded', decodedIdToken)
    // ID Token decodedをreq.userに格納
    req.user = decodedIdToken
    return next()
  } catch (error) {
    console.error('Error while verifying Firebase ID token:', error)
    return res.status(403).send('Unauthorized')
  }
}

// よくある認証APIとDBアクセス（バックエンドのみ許可）を組み合わせてDB操作を行う
async function info(req, res) {
  const id = req.user.uid
  const msg = req.body.msg
  if (!msg) return res.status(400).json()
  // pathで格納される
  await database.ref(`/blog/${id}`).set(msg)
  // path以下の子がすべて取れる
  const result = await database.ref(`/blog/${id}`).once('value').then((snapshot) => snapshot.val())
  res.json(result)
}

app.use(
  '/user',
  express.Router()
    // ユーザ認証付きAPI
    // `Authorization` HTTP headerに`Bearer <Firebase ID Token>`が必須
    .post('/info', validate, info)
)
```

## クライアントから直接DBを参照させない
database.rules.jsonにDBのアクセス権限を記入することができます。（このファイルを`firebase deployコマンドでデプロイします`）
クライアントサイドから直接アクセスさせずにfunctionsのみからアクセスさせたい場合はすべてfalseにします。
この場合、前述のfunctions側のfirebase.initializeAppにて`admin認証情報.json`を指定しておく必要があります。

```database.rules.json
{
  "rules": {
    ".read": false,
    ".write": false,
  }
}
```

もしくはfirebaseの管理画面にて直接ルールの編集やシミュレーションを行うこともできます。
<img width="1205" alt="スクリーンショット 2018-12-21 1.38.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/d985a5fa-6d25-a31b-8d5b-aca8874eb1ba.png">

## リアルタイムデータを更新・取得する

よくある例としてはチャットなどのデータをリアルタイムに反映したい場合です。
RealTime DBをクライアントサイドからDBに直接書き込み・閲覧できるようにすることとReduxを組み合わせることで簡単に実現可能になります。
database.rules.jsonのchatパス以下をログイン済みユーザであれば誰でも書き込み・閲覧できるようにします。

```database.rules.json
{
  "rules": {
    ".read": false,
    ".write": false,
    "chat": {
      ".read": "auth !== null",
      ".write": "auth !== null"
    }
  }
}
```

src/lib/firebase.jsにDB操作のユーティリティ関数を作成しておきます。

```src/lib/firebase.js
firebase.initializeApp(config)
const database = firebase.database()

export const setData = (path, data) => {
  database.ref(path).set(data)
}

export const pushData = (path, data) => {
  database.ref(path).push().set(data)
}

export const updateData = (path, data) => {
  database.ref(path).update(data)
}

export const removeData = (path) => {
  database.ref(path).remove()
}

export const receiveData = (path, callback) => {
  database.ref(path).on('value', (snapshot) => {
    callback(snapshot.val())
  })
}
```

src/index.jsにてチャットの読み取り、送信処理を追加します。
ポイントはapiサーバが不要（直接DB）書き込みしているのと、
データ更新された場合に`database.ref(path).on`がコールバックされてreduxにデータが保存され、
各コンポーネントの表示が更新されます。

```src/index.js
import { signIn, signOut, authChange, apiEndpoint, pushData, receiveData } from './lib/firebase'

const LOGIN = 'user/LOGIN'
const CHAT = 'user/CHAT'

function userReducer(state = {uid: null, token: null, chats: []}, action = {}) {
  switch (action.type) {
    case LOGIN:
      return {
        ...state,
        uid: action.data.uid,
        token: action.data.token,
      }
    case CHAT:
      return {
        ...state,
        chats: action.chats,
      }
    default:
      return state
  }
}

// 認証系の処理（上で説明してあるので略）

function sendChat(msg) {
  return () => {
    return new Promise(() => {
      pushData('/chat', msg)
    })
  }
}

receiveData('/chat', (chats) => {
  store.dispatch({type: CHAT, chats})
})

const TopPage = connect(
  state => ({
    uid: state.userReducer.uid,
    chats: state.userReducer.chats,
  }),
  { sendChat }
)(({uid, info, chats, sendInfo, sendChat, getFilePermission}) => (
  <div>
    {uid === null ?
      <button onClick={() => signIn()}>Sign in with Google</button> :
      <button onClick={() => signOut()}>Sign out</button>
    }
    <button onClick={() => sendChat({msg: 'ほげ'})}>チャット送信</button>
    {Object.keys(chats).map(key => <div key={key}>{chats[key].msg}</div>)}
  </div>
))
```

## ファイルのアップロード・ダウンロード

storage.rulesにStoregeに置かれているファイルのアクセス管理を行うことができます。（このファイルを`firebase deployコマンドでデプロイします`）
今回は/public以下を公開フォルダにします。
`allow read, write;`の場合は特に制限がないため、だれでもファイルの改変、閲覧が可能です。

```storage.rules
service firebase.storage {
  match /b/{bucket}/o {
    match /public {
      match /{allPaths=**} {
        allow read, write;
      }
    }
  }
}
```

もしくはFirebase Storegeの管理画面から直接ルールの編集、シミュレーションを行うこともできます。
<img width="1399" alt="スクリーンショット 2018-12-21 2.29.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/b35a67a6-8eab-e867-0e6a-c180cfcaa8f4.png">

・Firebase Storageにファイルのアップロード
Fileオブジェクトとパスを指定すればそのままアップロードできる

```src/lib/firebase.js
firebase.initializeApp(config)
const storage = firebase.storage()

export const upload = (file, path) => {
  // ルートフォルダの参照
  const rootRef = storage.ref()
  // パスへの参照
  const targetRef = rootRef.child(path)
  targetRef.put(file)
}
```

Fileオブジェクトをinputタグなどから取得する

```src/index.js
import { upload } from './lib/firebase'

<input type="file" name="files[]" multiple onChange={(e) => {
  const files = e.target.files // FileList object
  for (let file of files) {
    upload(file, 'public/' + file.name)
  }
}}/>
```

・Firebase Storageからファイルのダウンロード
バケットのCORSの制限を変更します。（クロスドメインとなるため）

```cors.json
[
  {
    "origin": ["*"],
    "method": ["GET"],
    "maxAgeSeconds": 3600
  }
]
```

`gsutil cors set cors.json gs://<your-cloud-storage-bucket> `を実行してcors.jsonをアップロードしてバケットのCORS設定を適用します。

```src/lib/firebase.js
const firebaseApp = firebase.initializeApp(config)
const storage = firebaseApp.storage()

export const download = (path) => {
  // ルートフォルダの参照
  const rootRef = storage.ref()
  // パスへの参照
  const targetRef = rootRef.child(path)

  return targetRef.getDownloadURL().then((url) => {

    // データのBlob取得
    return fetch(new Request(url))
      .then((response) => response.blob())
      .then((blob) => ({url, blob}))

  }).catch((error) => {
    // 全エラーコードの一覧
    // https://firebase.google.com/docs/storage/web/handle-errors
    switch (error.code) {
      case 'storage/object-not-found':
        // ファイルがない
        break
      case 'storage/unauthorized':
        // ユーザのファイルへのアクセス権がない
        break
      case 'storage/canceled':
        // ユーザがアップロードをキャンセルした
        break
      case 'storage/unknown':
        // 不明なエラー、サーバ側の問題の可能性あり
        break
    }
  })
}
```

Blobオブジェクトはブラウザ間の差異があるので注意

```src/index.js
import { download } from './lib/firebase'
const filename = 'public/hoge.img'

<a href="#" onClick={(e) => {
   e.preventDefault()
   download(filename).then(({url, blob}) => {

     if (window.navigator.msSaveBlob) {
       window.navigator.msSaveBlob(blob, filename)

       // msSaveOrOpenBlobの場合はファイルを保存せずに開ける
       window.navigator.msSaveOrOpenBlob(blob, filename)
     } else {
       const blobUrl = window.webkitURL.createObjectURL(blob)
       const link = document.createElement('a')
       link.href = url
       link.click()
       window.URL.revokeObjectURL(blobUrl)
     }
   )
  }}>
  ダウンロード
</a>
```

## ファイルのセキュリティ（認可したユーザにのみダウンロードできるように制限する）

storage.rulesにprivate用のルールを追加します。

- write: ファイルの作成者ユーザのみ
- read: 認可を与えられた（uidがメタデータ付与された）ユーザのみ

```storage.rules
service firebase.storage {
  match /b/{bucket}/o {
    match /public {
      match /{allPaths=**} {
        allow read, write;
      }
    }
    
    match /private {
      match /{userId}/{allPaths=**} {
        allow write: if request.auth.uid == userId; 
        allow read: if resource.metadata[request.auth.uid] != null || request.auth.uid == userId; 
      }
    }
  }
}
```

functionsでファイルのメタデータを付与するAPIを作成します。

```functions/index.js

// ファイルのメタ情報に操作を認可するユーザuidを追加する
async function addFilePermission(req, res) {
  const path = req.body.path
  const fileRef = bucket.file(path)
  let result = await fileRef.getMetadata()
  console.log(result)
  const data = {
    metadata: {}, // カスタムメタデータ
  }
  // 認可するユーザのuidを追加する
  data.metadata[req.user.uid] = true
  result = await fileRef.setMetadata(data)
  console.log(result)

  res.json({message: 'success'})
}

app.use(
  '/user',
  express.Router()
    // ユーザ認証付きAPI
    // `Authorization` HTTP headerに`Bearer <Firebase ID Token>`が必須
    .post('/permission', validate, addFilePermission)
)
```

## ローカル起動

```
// SPA起動
$ yarn start
// Functions APIサーバ
$ firebase serve
```

## デプロイ
functions、ルールの設定ファイル、Firebase HostingにSPAフォルダのデプロイ

```
$ firebase deploy
```