リアルタイムなToDoアプリをMilkcocoaとJavaScriptで作る(チーム編)

Posted: / Tags: JavaScript



前々回前回に引き続き、ToDoアプリを作成します。前回までのソースコード一式は以下です。

以下の流れで行っていますが、今回は5.を実装します。

  1. データを保存しないToDoアプリを作る(前々回)
  2. Milkcocoaでデータを保存&リアルタイムに編集する(前々回)
  3. 認証機能利用の準備をする(前回)
  4. 自分だけしか編集できないようにする(前回)
  5. チームにしか編集できないようにする(今回)

どんな仕様にするか?

ユーザにどうやってチームを構成させるか?そこから考えていきましょう。ToDoを編集できる画面を「ToDoボード」と名付て、以下のような仕様にします。

  • ユーザ毎に一つのToDoボードを割り当てる
  • ユーザは自分のToDoボードに対して、全ての権限を持っている、このユーザをToDoボードのオーナーとする
  • 一つのToDoボードに対して一つのURLがある(http://{rootURL}/#{user_id})
  • ユーザは権限の無いToDoボードを開けないが、権限をリクエストできる
  • ユーザは自分のToDoボードに対して来たリクエストを承認することが出来る

データストアの構成・セキュリティルールを決める

ToDoの表示・追加の権限は、ユーザーのIDを使って、セキュリティルールで決めることが出来ます。

セキュリティルールは、データストアごとに決めるので、承認したユーザーもデータストアで管理する必要があります。

ユーザーIDごとにデータストアを作成し、その子データストアに、リクエストをしたユーザーを保存する「リクエスト用のデータストア」と、承認したユーザーを保存する「承認用のデータストア」を作成します。

上記のデータストア構成の場合、以下のようなセキュリティルールになります(セキュリティルール上でコメントは書くことは出来ないので、実際に記述する際はコメントを削除して下さい)。

セキュリティルールの書き方は、前回の記事や、ドキュメントをご覧下さい。

// account.subはログイン中のユーザーのIDにあたるもの

// リクエスト用のデータストア。誰でもリクエストを送れるようにすべてのユーザーにsetとon(set)を許可する。
todos/[userID]/requests {
  permit : set, on(set);
  rule : true;
}
// リクエスト用のデータストア。オーナーはすべての権限を持つ。
todos/[userID]/requests {
  permit : all;
  rule : account.sub == userID;
}
// 承認用のデータストア。オーナーだけが権限をもつ。
todos/[userID]/allows {
  permit : all;
  rule : account.sub == userID;
}
// ToDo保存用のデータストア。オーナーはすべての権限を持つ。
todos/[userID] {
  permit : all;
  rule : userID == account.sub;
}
// ToDo保存用のデータストア。承認用のデータストアに存在するユーザーは権限を持つ。
todos/[userID] {
  permit : all;
  rule : dataStore("todos/" + userID + "/allows").exists(account.sub);
}

コードを書く

「ソースだけで十分」という方は、以下からどうぞ。

コードを書く前に、必要な画面を把握しておきましょう。以下の3つの画面を、権限によって出し分けます。

  • ToDoを編集できる画面 (todos_view)
  • 権限をリクエストをできる画面 (request_access_view)
  • リクエストを承認する画面 (admin_view)

HTMLは以下のようになります。

<body>
  <div id="request_access_view" style="display:none;">
    <button id="request_btn">リクエストを送る</button>
  </div>

  <div id="admin_view" style="display:none;">
    <span>リクエスト待ち</span>
    <select id="requests"></select>
    <button id="allow_btn">リクエストを許可する</button>
  </div>

  <hr>

  <div id="todos_view" style="display:none;">
    <div>
      <input id="new_content" type="text" />
      <button id="create_btn">新しいToDoを追加</button>
    </div>
    <div id="todos">
    </div>
  </div>

  <button id="logout_btn">ログアウト</button>
</body>

では、以下のフローチャートに従ってコードを書いていきます。

ログイン処理

ログイン処理は、冒頭に貼った前回のコードの、getUser()とほぼ変わりませんが、以下が違います

  • ユーザーの名前をローカルストレージに保存する(承認の際に表示される名前に使う)
  • ログインが終わったらリロードする
// エラー処理は割愛しています
function getUser(callback) {
  milkcocoa.user(function(err, user) {

    if(user) {
      callback(null, user.sub);
    } else {
      lock.show(function(err, profile, token) {

        // 承認画面で表示される名前をローカルストレージに保存しておく。
        window.localStorage.setItem('todoapp-username', profile.name);

        milkcocoa.authWithToken(token, function(err, user) {
          // ログインが成功したらリロード
          setTimeout(function(){
            window.location.reload();
          },200);
        });
      });
    }
  });
}

ログイン後の処理

getUser()のコールバックを書いていきます。

ログイン中のユーザーID(user_id)と、ToDoボードの名前(URLの#以下:todos_id)を見て、どの画面を表示するかをわけます。

var todos_id = escapeHTML(location.hash.substr(1));
var todoDataStore = milkcocoa.dataStore("todos").child(todos_id);

// URLの#以下が変わってもリロードするようにしておきます
window.onhashchange = function(e){ window.location.reload(); };

getUser(function(err, user_id) {

  if(err) {

  }else{
    // URLの#以下が空だったら
    if(todos_id == '') location.hash = user_id; // hashが変わるのでリロードされる

    // ToDo画面の表示関数。
    create_todos_view(user_id, todoDataStore, function() {
      /* リクエスト送信画面の表示関数
         このコールバックは、権限がなければ呼び出されるようにする */
      create_request_access_view(user_id, todoDataStore);
    });

    // リクエスト承認画面の表示関数。オーナーであれば実行。
    if(todos_id == user_id) create_admin_view(user_id, todoDataStore);
  }
});

create_request_access_view()は、MilkcocoaのAPIが権限がない場合にerrを返すことを利用して、エラーが出た場合にだけ実行するようにします(後で記述します)。

リクエスト画面の表示

リクエスト画面を作成します。といっても、リクエストボタンだけです。

ボタンを押すと、リクエスト用のデータストアに、idに「ユーザーID」、usernameに「ログインの際にローカルストレージに保存した名前」を指定して保存します。

function create_request_access_view(user_id, todoDataStore) {
  // ビューの表示・非表示等は割愛
  var request_button = document.getElementById("request_btn");
  var requestDataStore = todoDataStore.child('requests');

  requestDataStore.on('set', function(setted) {
    window.alert("参加をリクエストしました。");
  });

  request_button.addEventListener("click", function(e) {
    // ログインの際に保存したユーザーの名前を取得
    var username = window.localStorage.getItem('todoapp-username');
    // リクエストデータストアに登録
    requestDataStore.set(user_id, {
      username : username
    });
  });
}

承認画面の表示

承認画面を表示します。

リクエストデータストアから、リクエストを取り出してselect要素につっこみます。許可ボタンで、selectで選択したユーザーを、承認用のデータストアに保存します。

function create_admin_view(user_id, todoDataStore) {
  // 画面の表示・非表示割愛
  var allow_button = document.getElementById("allow_btn");

  var requestDataStore = todoDataStore.child('requests');
  var allowDataStore = todoDataStore.child('allows');

  // リクエスト一覧の表示
  requestDataStore.stream().sort('desc').size(20).next(function(err, reqs) {
    reqs.forEach(function(req) {
      // 以下はリクエストしたユーザー情報を、selectに要素につっこむ関数
      render_request(req.id, req.value);
    });
  });

  allowDataStore.on('set', function(setted) {
    window.alert("リクエストを許可しました。");
  });
  allow_button.addEventListener("click", function(e) {
    // select要素で選択しているIDを、承認用のデータストアに登録
    var target_user_id = requests.options[requests.selectedIndex].dataset['user_id'];
    allowDataStore.set(target_user_id, {});
  });
}

ToDo画面の表示

ToDoを表示する画面をつくっていきます。

今までと同じようにstream()を使いますが、権限を持っていない(アクセスが許可されていない)ユーザーにはリクエスト画面を表示したいです。

これは、ログイン後の処理でのべたように、MilkcocoaのAPIがアクセスが許可されていない状態でメソッドを使うとコールバックの第一引数(慣習的にerr)にエラーを返すことを利用します。

function create_todos_view(user_id, todoDataStore, callback) {

  // 表示処理は割愛

  todoDataStore.stream().sort('desc').size(20).next(function(err, todos) {

    if(err) {
      // 権限がなければリクエスト画面だけを表示
      callback(); // create_request_access_view()を実行
      return;
    }
    // 権限があれば、ToDoを表示
    todos.forEach(function(todo) {
      render_todo(todo.value);
    });
  });

  // pushやonは割愛
}

これで、特定のメンバーだけで利用できるToDoアプリが出来たかと思います。

まとめ

3回にわたって、プライベートなToDoアプリの作り方を説明してきました。

この考え方を基本に、見た目やインタラクションを加えるだけで、立派なWebサービスを作ることができます。

特にスタートアップの皆さんは、認証機能を使ったプライベートアプリをこれだけ簡単に実装できれば、プロトタイプも高速に開発できるのではないでしょうか。

また今回は、ToDoアプリと言いながら「ToDoを追加する」機能しか実装していません(ToDoというよりチャットですね)。

なので、もしかすると、番外編として「BaaSのMVC設計」のようなタイトルで、すべての機能を持ったToDoアプリの作成方法を投稿するかもしれないので、少しだけ期待をして頂けると幸いです。