Web3.0検証(19)-MeteorでTODO管理アプリの開発(データアクセス制御)

##### [前回] [Web3.0検証(18)-MeteorでTODO管理アプリの開発(Githubアカウントでログイン)](https://qiita.com/mingchun_zhao/items/8bcfdcd53ed45c9d00b3)

## はじめに

今回は、セキュリティ関連のデータ保護機能です。
Meteorメソッドを使用し、データへのアクセスを制御します。

## データアクセス制御の必要性

- アプリケーションで、データへのアクセスを制御しないと
- クライアントからデータを直接操作できるなど、セキュリティ上問題となる

## Meteorメソッドによるデータ操作

- Meteorメソッドとは、`Meteor.call`関数を使用しサーバーと通信する方法
    - メソッドの名前と引数を指定する必要あり
- Meteorでメソッドを宣言することで、安全にデータ操作できる
    - メソッドを使用し、ユーザーのデータ操作に対し認証/認可を行う
- クライアントからの以下直接呼び出しをブロック
    - `insert`
    - `update`
    - `remove`

## 準備

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

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

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

## 安全でないパッケージを無効に

- `insecure`パッケージ
    - クライアントからデータベースを編集可能にできる
    - 迅速なプロトタイピングに役立つ

- 端末2から、安全でないパッケージを削除
```
$ cd simple-todos-react
$ meteor remove insecure
```

- クライアント側のデータベース権限をすべて取り消したため
    - アプリの変更は機能しない
        - たとえば、新しいタスクを挿入すると失敗
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/389d0eeb-f648-fff9-883e-6f7260b3c4dd.png)

## タスクメソッドを追加

- 作業内容
    - メソッドを定義
        - クライアントで実行するデータベース操作ごとに1つのメソッドが必要
        - Optimistic UIをサポートするためには、クライアントとサーバー両方で実行されるコードで定義する必要あり
    - Optimistic UI
        - `Meteor.call`を使用し、クライアントでメソッドを呼び出すと、次の2つが並行して発生
            - クライアントはサーバーにリクエストを送信し、安全な環境でメソッドを実行
            - メソッドのシミュレーションがクライアントで直接実行され、呼び出しの結果を予測
            - サーバーから結果が返される前に、新しく作成されたタスクが実際に画面に表示される
            - 予測結果がサーバーの結果と一致する場合、すべてがそのまま残る
            - さもなければ、UIはサーバーの実際の状態を反映するため更新される

- コード
    - `imports/api`フォルダーに`tasksMethods`ファイルを追加
        - クライアントは、Mini Mongo操作を直接使用する代わりに、これらのメソッドを呼び出す
        - メソッド内に、オブジェクトで使用できる特別なプロパティが存在
            - たとえば、認証されたユーザーの`userId`
    - `check`パッケージを使用し、入力値が期待されるタイプか確認
        - データベースに対し、何を挿入/更新しているかを正確に把握するために重要
```imports/api/tasksMethods.js
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { TasksCollection } from './TasksCollection';
 
Meteor.methods({
  'tasks.insert'(text) {
    check(text, String);

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

    TasksCollection.insert({
      text,
      createdAt: new Date,
      userId: this.userId,
    })
  },

  'tasks.remove'(taskId) {
    check(taskId, String);

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

    TasksCollection.remove(taskId);
  },

  'tasks.setIsChecked'(taskId, isChecked) {
    check(taskId, String);
    check(isChecked, Boolean);
 
    if (!this.userId) {
      throw new Meteor.Error('Not authorized.');
    }

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

- サーバーにこれらのメソッドが登録されているか確認
    - `tasksMethods`ファイルをインポート
    - インポートからシンボルを取得する必要なし
        - ファイルをインポートするようにサーバーに要求するだけで
            - `Meteor.methods`が評価され、サーバー起動時にメソッドが登録される
```server/main.js
import '/imports/api/tasksMethods';
```

## メソッド呼び出しを実装

- 作業内容
    - メソッドを使用するため、コレクションの操作箇所を更新
    - `TaskForm`ファイルで
        - `TasksCollection.insert`の代わりに、`Meteor.call('tasks.insert', text);`を呼び出す
        - インポートも修正
        - サーバーで`userId`を取得する
            - `TaskForm`コンポーネントがユーザーを取得する必要はなくなる
- コード
```imports/ui/TaskForm.jsx
export const TaskForm = () => {
  const [text, setText] = useState('');

  const handleSubmit = e => {
    e.preventDefault();

    if (!text) return;

    Meteor.call('tasks.insert', text);

    setText('');
  };
  ... ...
};
```

- `App.jsx`ファイルで
    - `TasksCollection.update`の代わりに、`Meteor.call('tasks.setIsChecked', _id, !isChecked);`をコール
    - `TasksCollection.remove`の代わりに、Meteor.call('tasks.remove', _id)`をコール
    - `<TaskForm/>`からユーザープロパティも削除
```imports/ui/App.jsx
import { Meteor } from 'meteor/meteor';
import React, { useState, Fragment } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { TasksCollection } from '/imports/db/TasksCollection';
import { Task } from './Task';
import { TaskForm } from './TaskForm';
import { LoginForm } from './LoginForm';

const toggleChecked = ({ _id, isChecked }) =>
  Meteor.call('tasks.setIsChecked', _id, !isChecked);

const deleteTask = ({ _id }) => Meteor.call('tasks.remove', _id);
... ...

            <TaskForm />
.....
```

## アプリを確認

- 今までの修正のおさらい
    - データベースにタスクを挿入すると
        - ユーザー認証によるチェックが実行される
    - クライアントコードをデータベースロジックから分離
        - データ変更は、イベントハンドラーの代わりに、メソッド呼び出しを使用

- 画面から、新しいタスクを登録

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/167e3ccc-e084-eb35-a396-9284918645a0.png)

タスクが正しく登録できました。やったー。

## ファイル配置を変更

- `TasksCollection.js`ファイルを`api`フォルダから`db`フォルダに移動
    - プロジェクトで`API`は、サーバーとクライアント間の通信レイヤーを意味
    - `TasksCollection`コレクションは、上記修正により既にこの役割を果たしていない
    - ※ ファイルや変数の名前は現実と同じものが推奨される

- インポートを修正
    - 以下四つのファイルに`TasksCollection`へのインポートが存在
        - imports/api/tasksMethods.js
        - imports/ui/TaskForm.jsx
        - imports/ui/App.jsx
        - server/main.js
    - 変更前: `import { TasksCollection } from '/imports/api/TasksCollection';`
    - 変更後: `import { TasksCollection } from '/imports/db/TasksCollection';`


## トラブルシューティング

- Meteor DevToolsを使用
    - サーバーへ送信されるメッセージと返される結果を確認可能
    - ブラウザのデベロッパーツールで、[DDP]タブから確認可能
        - DDPは、Meteor通信レイヤーのバックグラウンドのプロトコル
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/43688772-89f2-1bfe-904a-d2641c98ec08.png)

- `check`呼び出しにエラーハンドリングを追加
    - 間違ったタイプに対し、発生したエラーを追跡できる

## おわりに

アプリのセキュリティ対策として、データアクセス制御を追加しました。
次回も続きます。お楽しみに。