Web3.0検証(20)-MeteorでTODO管理アプリの開発(publish/subscribeによるデータ制御)

##### [前回] [Web3.0検証(19)-MeteorでTODO管理アプリの開発(データアクセス制御)](https://qiita.com/mingchun_zhao/items/fef9d71e03962042f999)

## はじめに

前回に続き、セキュリティ関連のデータ保護機能です。
publish/subscribe機能を用いて、データ同期を制御することで、
サーバー側の機微な情報が、誤ってクライアントに送信されないように保護します。

## 理論: publish(公開)/subscribe(購読)によるデータ同期

  `[クライアント(subscribe)]  <--- (データ) --->  [(publish)サーバー]`

- Meteorでは，サーバー上のデータはクライアントにキャッシュされ，リアルタイムに同期される
    - 同期は自動的に、双方向で行われる
- 開発者が明示的に同期処理を記述する必要はない
    - `Meteor.publish()`と`Meteor.subscribe()`を使用し，データ同期の範囲を指定するのみ

## 準備

- 端末を二つ用意
    - 端末1: meteor起動/確認
    - 端末2: コード修正

- 端末1から、Meteorアプリを実行
```
$ cd simple-todos-react
$ meteor run
```

- ブラウザでアプリを確認
    - `http://localhost:3000/`にアクセス
    - デベロッパーツールを開く(F12)
    - `デバイスのツールバーを切り替え`アイコンをクリック
    - デバイスを選択
        - 今回は、`iPhone SE`を選択

## publish/subscribeの実装

- 今まで、クライアントにデータベースのデータが自動同期されていた
    - `Tasks.find()`を呼び出すと、コレクション内のすべてのタスクが取得される
    - クライアントに個人情報など保存すると、プライバシーが侵害される恐れがある
- クライアント側に送信するデータを制御する必要あり

### `autopublish`パッケージを削除

- `autopublish`パッケージにより、データベースコンテンツがクライアントに自動同期される
- 端末2から、安全でないパッケージを削除
```
$ cd simple-todos-react
$ meteor remove autopublish
```

- アプリが更新されると、タスクリストは空になる
    - `autopublish`パッケージがないので、サーバーからクライアントへの送信データを明示的に指定する必要があるため
- データ同期に使用されるMeteor関数
    - `Meteor.publish`: サーバーからクライアントにデータを公開する
    - `Meteor.subscribe`: クライアントで、サーバーから公開されたデータを購読(⁠同期して取り込む)

### タスクの公開(publish)

- 最初にパブリケーションをサーバーに追加
    - 認証されたユーザーからのすべてのタスクを公開
    - `Methods`と同様、パブリケーション関数で`this.userId`を使用し、認証された`userId`を取得可能
- `api`フォルダーに`tasksPublications.js`ファイルを新規作成
```imports/api/tasksPublications.js
import { Meteor } from 'meteor/meteor';
import { TasksCollection } from '/imports/db/TasksCollection';

Meteor.publish('tasks', function publishTasks() {
  return TasksCollection.find({ userId: this.userId });
});
```

- この関数内で`this`を使用しているため、アロー関数(arrow function)`=>`は使用できない
    - アロー関数は`this`コンテキストを提供しないため
    - 代わりに、`function`キーワードを用いて、従来の関数定義を使用
- サーバーがパブリケーションを登録していることを確認
    - `server/main.js`で、`tasksPublications`をインポートし、強制的に評価

```server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/db/TasksCollection';
import '/imports/api/tasksMethods';
import '/imports/api/tasksPublications';
```

### タスクの購読(subscribe)

- クライアントで上述パブリケーションをサブスクライブ
    - パブリケーションから変更を受け取るため、`useTracker`フック内で`subscribe`を呼び出す
- 単一の`useTracker`を使用し、`TasksCollection`からデータ取得するように、ついでにコードをリファクタリング
```imports/ui/App.jsx
... ...
  const { tasks, pendingTasksCount, isLoading } = useTracker(() => {
    const noDataAvailable = { tasks: [], pendingTasksCount: 0 };
    if (!Meteor.user()) {
      return noDataAvailable;
    }
    const handler = Meteor.subscribe('tasks');

    if (!handler.ready()) {
      return { ...noDataAvailable, isLoading: true };
    }

    const tasks = TasksCollection.find(
      hideCompleted ? pendingOnlyFilter : userFilter,
      {
        sort: { createdAt: -1 },
      }
    ).fetch();
    const pendingTasksCount = TasksCollection.find(pendingOnlyFilter).count();

    return { tasks, pendingTasksCount };
  });
... ...
  <div className="main">
    {user ? (
      <Fragment>
    <div className="user" onClick={logout}>
      {user.username || user.services ? user.services.github.username : ''} 🚪
    </div>

    <TaskForm />
```

### 読み込み中ステータス表示

- サブスクリプションデータが準備できていない場合、ユーザーに知らせる必要あり
    - アプリのローディングステータスを画面表示
- サブスクリプションの準備ができているかの確認方法
    - `subscribe`呼び出しの結果を確認
        - リターン値は、サブスクリプション状態オブジェクトで、`boolean`を返す`ready`関数が含まれる
```imports/ui/App.jsx
... ...
            <div className="filter">
              <button onClick={() => setHideCompleted(!hideCompleted)}>
                {hideCompleted ? 'Show All' : 'Hide Completed'}
              </button>
            </div>

            {isLoading && <div className="loading">loading...</div>}

            <ul className="tasks">
... ...
```

- 読み込み中表示のスタイル設定

```client/main.css
.loading {
  display: flex;
  flex-direction: column;
  height: 100%;

  justify-content: center;
  align-items: center;

  font-weight: bold;
}
```

- 上記修正が反映されると、すべてのタスクが再表示される
    - サーバーで、`Meteor.publish`を呼び出すと
        - `tasks`という名前のパブリケーションが登録される
    - クライアントで、`Meteor.subscribe`をパブリケーション名で呼び出すと
        - クライアントはパブリケーションからの下記データをサブスクライブ
            - 認証されたユーザーが所有するデータベース内の全タスク

### ユーザー権限を確認

- 現状、誰でもブラウザ`console`を使用し、Meteor`Methods`を呼び出せる
    - 例えば、`remove`メソッドから`validation`を削除し、有効なtask `_id`を渡すと、削除できてしまう
    - 試しに、DevToolsコンソールタブを使用し、以下のようなコマンドを入力し、Enter
    `Meteor.call('tasks.remove'、'xtPTsNECC3KPuMnDu');`
- タスク所有者のみ、特定タスクを変更可能に制限する必要あり
    - メソッドを変更し、認証されたユーザーがタスク作成ユーザーと一致するか確認
```imports/api/tasksMethods.js
... ...
  'tasks.remove'(taskId) {
    check(taskId, String);

    if (!this.userId) {
      throw new Meteor.Error('Not authorized.');
    }

    const task = TasksCollection.findOne({ _id: taskId, userId: this.userId });

    if (!task) {
      throw new Meteor.Error('Access denied.');
    }

    TasksCollection.remove(taskId);
  },

  'tasks.setIsChecked'(taskId, isChecked) {
    check(taskId, String);
    check(isChecked, Boolean);

    if (!this.userId) {
      throw new Meteor.Error('Not authorized.');
    }

    const task = TasksCollection.findOne({ _id: taskId, userId: this.userId });

    if (!task) {
      throw new Meteor.Error('Access denied.');
    }

    TasksCollection.update(taskId, {
      $set: {
        isChecked,
      },
    });
  },
... ...
```

## 再度、ブラウザからアプリを確認

- ログイン
    - ユーザー名: meteorite
    - パスワード: password

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/80138774-323a-8c7a-6166-a8f3e8b168f2.png)

ログインユーザーのタスク一覧が正常に表示されました。

## おわりに

publish/subscribeを用いて、クライアント/サーバー間のデータやり取りを制御しました。
次回も続きます。お楽しみに。

##### [次回] [Web3.0検証(21)-MeteorでTODO管理アプリの開発(リグレッションテスト)](https://qiita.com/mingchun_zhao/items/5f879f2dc6e2979904e6)