Firebase Hostingの１つのプロジェクトでUser向けサイトとAdmin向けサイトを運営する 〜で、Basic認証もする〜


Firebase Hostingは簡単なサイトを公開するには簡単で便利なサービスです。
ここでは1つのプロジェクトにて復数のサイトを運営し、かつ、それぞれにBaic認証等をかける方法についてまとめてみます。

## やりたいこと

* 1つのプロジェクトでUser向けサイトとAdmin向けサイトを運営する
* 各サイトのコンテンツはSPA（ReactかつReactRouterを利用する）
* 各サイトに別々にBasic認証をかけたい（アカウント、有効無効を分けたい）


## 手順

* Hostingにて復数サイトを作成し、それぞれにtargetを設定する
* 各targetにて、全てのリクエストをfunctionsに仕向けるようにする
* user用はappUser, admin用はappAdmin functionに仕向ける
* 各functionで表示する(SPA)コンテンツを指定する
* 各functionにてBasic認証の設定をする

## 前提

* Firebaseのアカウント、プロジェクトが作成されHostingとFunctionsが利用できる状態となっている
  * ソースロケーションはasia-northeast1（必須ではない）
* firebase-toolsが利用できる状態にある


## 実装：準備

まず、作業場所を作ります。普通にmkdir。

```bash
mkdir web-test
cd web-test
```

で、firebase init。
とりあえずFunctionsとHostingを選んでおきます。

```bash
firebase init

? Which Firebase CLI features do you want to set up for this folder? Press Space
 to select features, then Enter to confirm your choices.
 ◯ 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
 ◯ Emulators: Set up local emulators for Firebase features
```

existingプロジェクトを選びます。お好みで。

```bash
? Please select an option: (Use arrow keys)
❯ Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project
```

ここではmulti-site-43c8bというプロジェクトを使います。

```bash
? Select a default Firebase project for this directory:
❯ multi-site-43c8b (multi-site)
```

とりあえずJavaScript。

```bash
? What language would you like to use to write Cloud Functions? (Use arrow keys)

❯ JavaScript
  TypeScript
```

Lintはいらない。dependenciesはインストールしておきます。

```bash
? Do you want to use ESLint to catch probable bugs and enforce style? No
? Do you want to install dependencies with npm now? Yes
```

publicのままで。

```bash
? What do you want to use as your public directory? (public)
```

どっちでもいいのですが、生成されるfirebase.jsonにrewrite記述を入れておきたいのでyを選択。


```bash
? Configure as a single-page app (rewrite all urls to /index.html)? (y/N) y
```

こんな構造になっているはず。

```bash
.
├── firebase.json
├── functions
│   ├── index.js
│   ├── node_modules
│   ├── package-lock.json
│   └── package.json
└── public
    └── index.html
```

## 実装：サイトを構成する

では復数サイト（ここでは２つ）を構成していきます。

### サイトの追加

既にデフォルトサイトはあるので、１つサイトを追加します。Hostingページの下の方に書きボタンがあります。

![スクリーンショット 2019-12-06 7.49.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/dadf2d05-34bf-1202-f111-760f8b2db213.png "スクリーンショット 2019-12-06 7.49.35.png")

ProjectIdがサブドメインになっているので、-adminを追加したサイトを追加してみます。


![スクリーンショット 2019-12-06 7.50.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/a6147216-a474-8d5c-ac7e-8e8facfb5bdf.png "スクリーンショット 2019-12-06 7.50.13.png")

１つ追加され、２つのサイトが確認できます。


![スクリーンショット 2019-12-06 7.50.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/753b9fa5-c09a-c6eb-8609-eff016a57eee.png "スクリーンショット 2019-12-06 7.50.41.png")

### サイトの構成（target設定）

コマンドで各サイトを管理・操作するためにtarget名を設定しておきます。

firebase target:apply hosting [ターゲット名] [サイト名]

の形式で設定できます。
デフォルトのサイトにuser-site, 追加したadmin用（を想定した）サイトにadmin-siteと名前を付けます。

```bash
firebase target:apply hosting user-site fire
firebase target:apply hosting admin-site multi-site-43c8b-admin
```

.firebasercが下記のようになっています。

```bash:.firebaserc
{
  "projects": {
    "default": "multi-site-43c8b"
  },
  "targets": {
    "multi-site-43c8b": {
      "hosting": {
        "user-site": [
          "multi-site-42c7a"
        ],
        "admin-site": [
          "multi-site-42c7a-admin"
        ]
      }
    }
  }
}
```

### firebase.jsonの設定

ターゲットが設定できたところ、各サイトをただしく管理できるよう、firebase.jsonの設定を下記のように変更します。
復数サイトの情報を管理する場合は、hosting要素を配列に、各サイトの設定を行っていきます。
いろいろ書いていますが、、

* user-siteの公開ディレクトリをpublicに、全てのリクエストをその中のindex.htmlで処理（SPA想定）
* admin-siteの公開ディレクトリをpublic-adminに、全てのリクエストをその中のindex.htmlで処理（SPA想定）

というようなところがポイントです。


```json:firebase.json
{
  "hosting": [
    {
      "target": "user-site",
      "public": "public",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "admin-site",
      "public": "public-admin",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }
  ]
}
```

### 動作確認

では各サイトに実際にコンテンツを配置・デプロイして動作を確認してみます。
publicおよびpublic-admin内に簡単なindex.htmlを作成してデプロイしてみます。

#### public/index.html

ユーザーサイト向けHTMLの内容。

```html:index.html
<!DOCTYPE html>
<html>
  <body>User-Site</body>
</html>
```

#### public-admin/index.html

public-adminディレクトリは存在しないので、作成します。

```bash
mkdir public-admin
touch public-admin/index.html
```

adminサイト向けのHTMLの内容。

```html:index.html
<!DOCTYPE html>
<html>
  <body>Admin-Site</body>
</html>
```

現状でこんな構造かと思います。

```bash
.
├── firebase.json
├── functions
│   ├── index.js
│   ├── node_modules
│   ├── package-lock.json
│   └── package.json
├── public
│   └── index.html
└── public-admin
    └── index.html
```

#### deploy

準備ができたらdeployしてみます。

```bash
firebase deploy
```

>今回はHosting関連だけの更新なのでfirebase deploy --only hostingでもかまいません。

deployが完了すると下記のようにURLが表示されますので、それぞれにアクセスしてみてください。

>URLは各自の環境で違います。

```bash
✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/multi-site-43c8b/overview
Hosting URL: https://multi-site-43c8b.firebaseapp.com
Hosting URL: https://multi-site-43c8b-admin.firebaseapp.com
```

それぞれのURLでユーザー用、アドミン用のコンテンツが見えるはずです。

復数サイトを公開したいだけなら、ここまでの操作でいいのですが、実運用を前提とすると各サイトにBasic認証等をかける必要があります（他にもいろいろありますが）。ただ、現状、Firebase Hostingではそれらの機能が存在しないためFunctions等の機能を応用して実現する必要があります。

具体的にはHTTP/HTTPSリクエストをFunctionsに振り分けて、そこで認証処理を行ったのち、Webコンテンツにリダイレクト（正確には違います）するような感じです。
下記のようなイメージでしょうか。

[リクエスト] → [functions(認証：OKなら)] → [コンテンツ表示]


## 実装：リクエストをfunctionsに仕向ける

では、設定していきます。
まず、Functionsのプログラム内で利用するモジュールをインストールしておきます。

>作業はfunctionsフォルダの中で行ってください。のちのち使うモジュールも入れておきます。

```bash
cd functions
npm install --save express path basic-auth-connect
```

exprssの機能をつかって実装します。
ユーザーサイトを処理するexpressインスタンスとアドミンサイトを処理するインスタンスをを作成し、レスポンスを返すようにしています。

```js:index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const basicAuth = require('basic-auth-connect');

const appUser = express();
const appAdmin = express();

//userサイトの処理
appUser.all("*", (req, res, next) => {
    res.send("user site from function");
})

//adminサイトの処理
appAdmin.all("*", (req, res, next) => {
    res.send("admin site from function");
})

//関数公開
exports.appUser = functions.https.onRequest(appUser);
exports.appAdmin = functions.https.onRequest(appAdmin);
```

リスエストがFunctionsに割り振られるようにfirebase.jsonの設定を変更します。

```diff:firebase.json
{
  "hosting": [
    {
      "target": "user-site",
      "public": "public",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
+          "function": "appUser"
        }
      ]
    },
    {
      "target": "admin-site",
      "public": "public-admin",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
+          "function": "appAdmin"
        }
      ]
    }
  ]
}
```

なお、"public"で指定したフォルダ内にコンテンツがあると、仕向設定をしてもそちらが優先されるようなので、publicおよびpublic-adminの中のコンテンツ(index.html)は削除しておきます。

```bash
cd ..
rm public/index.html
rm public-admin/index.html
```

作業が終わればデプロイします。

```bash
firebase deploy
```

>なお、functionは明示的にリージョンを指定しないとus-central1にデプロイされます。リージョンを指定することもできますが、HTTPのrewirte先となるfunctionはus-central1である必要があるので、一旦そのままで大丈夫です。詳しくは[こちら](https://qiita.com/qoAop/items/249c1f60b4ec2dd286b9)。

デプロイが完了したら表示を確認してみてください。

## 実装：表示先をSPA（React）コンテンツにする

上記サンプルではダイレクトに文字列を返していただけですが、実運用を想定してSPAコンテンツを返してみます。
functionsフォルダ内でreactプロジェクトを作成してみます。

>VueやAngularでも基本的には同じです。

ユーザー用とアドミン用のreactアプリを生成します。

```bash
cd functions
create-react-app user-site --use-npm
create-react-app admin-site --use-npm
```

デフォルト画面のままでもテストはできますが、振り分けがちゃんとできているか確認しづらいのでそれぞれのApp.jsを編集します。

#### user-site/src/App.js

```js:user-site/src/App.js
import React from 'react';
import './App.css';

function App() {
  return (
    <div>
      <p>User-Site from React</p>
    </div>
  );
}

export default App;
```

#### admin-site/src/App.js

```js:admin-site/src/App.js
import React from 'react';
import './App.css';

function App() {
  return (
    <div>
      <p>Admin-Site from React</p>
    </div>
  );
}

export default App;
```

編集が終わったら、user-site, admin-site各フォルダ内でbuildを実行します。

```bash
npm run build
```

現状で以下のような構造かと思います。
user-site, admin-site以下にbuildフォルダが生成されていることを確認してください。

```bash
.
├── firebase.json
├── functions
│   ├── admin-site
│   │   ├── README.md
│   │   ├── build #admin公開サイト
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── public
│   │   └── src
│   ├── index.js
│   ├── package-lock.json
│   ├── package.json
│   └── user-site
│       ├── README.md
│       ├── build #user公開サイト
│       ├── package-lock.json
│       ├── package.json
│       ├── public
│       └── src
├── public
└── public-admin
```

各サイト内のコンテンツを正しく表示できるようにfunction/index.jsを編集します。

```js:index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const basicAuth = require('basic-auth-connect');

const appUser = express();
const appAdmin = express();

//userサイトの処理 ----------------------------------------------------------

//staticコンテンツ指定
appUser.use(express.static(path.join(__dirname, 'user-site', 'build')));
//すべてのリクエストをuser-site/build/index.htmlで処理する
appUser.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'user-site', 'build', 'index.html'));
})

//adminサイトの処理 ----------------------------------------------------------

//staticコンテンツ指定
appAdmin.use(express.static(path.join(__dirname, 'admin-site', 'build')));

//すべてのリクエストをadmin-site/build/index.htmlで処理する
appAdmin.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'admin-site', 'build', 'index.html'));
})

//関数公開
exports.appUser = functions.https.onRequest(appUser);
exports.appAdmin = functions.https.onRequest(appAdmin);
```


編集が終わったらデプロイします。

```bash
firebase deploy
```


アクセスした際、Permissionエラーがでる場合があります。その場合、GCPコンソールのCloud Functionsで、ユーザーとパーミッションを設定します。

![スクリーンショット 2019-12-06 9.35.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/5c0df114-200e-ef54-a0bd-4c2f563febfb.png "スクリーンショット 2019-12-06 9.35.38.png")


[ユーザーの追加]でallUsersを選び、Cloud Functions 起動元の権限を与えます。

![スクリーンショット 2019-12-06 9.38.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/20daed74-b5d7-21e3-d301-daf42589ab0b.png "スクリーンショット 2019-12-06 9.38.32.png")


設定が完了したら再度アクセスしてみてください。
Reactが生成するコンテンツが表示されるはずです。


## 実装：Basic認証をかける

では、Basic認証をかけてみます。
ここまでの作業がうまくいっていれば、追加は簡単です。

ここではuser-siteのみにBasic認証をかけてみます。

```diff:index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const basicAuth = require('basic-auth-connect');

const appUser = express();
const appAdmin = express();

//userサイトの処理 ----------------------------------------------------------

//basic auth
+appUser.use(basicAuth('user','password'));

//staticコンテンツ指定
appUser.use(express.static(path.join(__dirname, 'user-site', 'build')));
//すべてのリクエストをuser-site/build/index.htmlで処理する
appUser.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'user-site', 'build', 'index.html'));
})

//adminサイトの処理 ----------------------------------------------------------

//staticコンテンツ指定
appAdmin.use(express.static(path.join(__dirname, 'admin-site', 'build')));

//すべてのリクエストをadmin-site/build/index.htmlで処理する
appAdmin.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'admin-site', 'build', 'index.html'));
})

//関数公開
exports.appUser = functions.https.onRequest(appUser);
exports.appAdmin = functions.https.onRequest(appAdmin);
```


デプロイして動作を確認します。

```bash
firebase deploy
```

## Functionsを経由しないようにする

functionsはしばらく利用しないと眠ってしまうのと、無駄にus-central1を経由するのでレスポンスが落ちます（そのはず）。
なので、本番環境に切り替える場合はfunctionsを経由しないよう、直接コンテンツを見に行くようにしたほうがいいでしょう。

```diff:firebase.js
{
  "hosting": [
    {
      "target": "user-site",
+      "public": "user-site/build",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
+          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "admin-site",
      "public": "public-admin",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "function": "appAdmin"
        }
      ]
    }
  ]
}
```

かるく計測してみたところ、Functions経由なしでは10ms以下でresponseが完了しますが、Functions経由だと200ms前後かかるようです。

## React-Routerが正しくルーティングされているか確認する

SPAの場合、ルート(/)ではうまく動いても、それ以外では動かないということがよくあります。
自分が利用しているフレームワークやルーティングモジュールで正しく動作するか確認したほうが無難です。

ここではreact-router利用下で正しくルーティングが動作するか確認してみます。

user-site, admin-siteそれぞれの下で、

```bash
npm install --save react-router-dom
```

を実行して、react-routerが利用できるようにします。そのうえで、各、App.jsを編集します。

#### user-site/src/App.js

```js:user-site/App.js
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" render={()=><p>Home@User</p>}/>
        <Route exact path="/about" render={()=><p>About@User</p>}/>
        <Route render={()=><p>Page not found@User</p>}/>
      </Switch>
    </BrowserRouter>
  );
}

export default App;
```


#### admin-site/src/App.js

```js:admin-site/App.js
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" render={()=><p>Home@Admin</p>}/>
        <Route exact path="/about" render={()=><p>About@Admin</p>}/>
        <Route render={()=><p>Page not found@Admin</p>}/>
      </Switch>
    </BrowserRouter>
  );
}

export default App;
```

編集が終わったらそれそれのアプリ内でbuildを実行します。

```bash
npm run build
```

buidが完了したらデプロイします。

```bash
firebase deploy
```

動作を確認してみます。
各サイトで、/aboutや/xxxxとした際に正しく動作してるか確認してみてください。

とりあえず以上です。