LoginSignup
2

More than 5 years have passed since last update.

MeteorのTutorialを試してみる(4

Posted at

https://www.meteor.com/try/9
前回に引き続き、MeteorのTutorialに挑戦していきます。
今回で終わらせる予定です。ユーザアカウントを追加し、プライベートタスクやセキュリティ周りをやっていきます。

Adding user accounts

Meteorはアカウントシステムを内蔵しているそうです。
有効にするために次のコマンドをたたきます。

$ meteor add accounts-ui accounts-password

Changes to your project's package version selections:

accounts-base          added, version 1.1.3   
accounts-password      added, version 1.0.6
accounts-ui            added, version 1.1.4
accounts-ui-unstyled   added, version 1.1.6
email                  added, version 1.0.5
less                   added, version 1.0.12
localstorage           added, version 1.0.2
npm-bcrypt             added, version 0.7.7
service-configuration  added, version 1.0.3
sha                    added, version 1.0.2
srp                    added, version 1.0.2


accounts-ui: Simple templates to add login widgets to an app
accounts-password: Password support for accounts
$  

できましたね。では早速使いましょう。{{> loginButtons}}をHide Completed Tasksラベルの下にでも配置してください。

simple-todos.html
  <h1>Todo List ({{incompleteCount}})</h1>
  <label class="hide-completed">
    <input type="checkbox" checked="{{hideCompleted}}" />
      Hide Completed Tasks
    </label>
    {{> loginButtons}}

また以下のコードをsimple-todos.jsに追加します。
デフォルトではe-mailをログインに使用しますが、これによりユーザネームでログインできるようになります。

・・・ simple-todos.js
Accounts.ui.config({
passwordSignupFields: "USERNAME_ONLY"
});
・・・

これで以下のようなログインボタンが表示されます。
image

クリックするとダイアログが表示されます。
image

では次にToDoを誰が作成したかわかるようにする機能を追加します。タスク登録箇所を次のようにします。

simple-todos.js
・・・
      Tasks.insert({
        text: text,
        createdAt: new Date(), // current time
        owner: Meteor.userId(), // _id of logged in user
        username: Meteor.user().username // username of logged in user
      });
・・・

みたまんまですが、タスクドキュメントにユーザIDとユーザ名を追加しています。なおIDはMeteorが自動的にアカウントに付与するIDになります。

では次にタスクにユーザ名を表示しましょう。
simple-todos.htmlのタスク名表示部を以下で置き換えます。

simple-todos.html
<span class="text"><strong>{{username}}</strong> - {{text}}</span>

またログインしていないユーザは、タスクを登録できなくしましょう。
simple-todos.htmlを次のようにします。

・・・
    <header>
      <h1>Todo List ({{incompleteCount}})</h1>
        <label class="hide-completed">
          <input type="checkbox" checked="{{hideCompleted}}" />
          Hide Completed Tasks
        </label>
         {{#if currentUser}}
           <form class="new-task">
            <input type="text" name="text" placeholder="Type to add new tasks" />
          </form>
         {{/if}}
        {{> loginButtons}}
    </header>
・・・

これもそのまんまですが、currentUserはユーザがログインしている場合のみ存在します。それを条件に表示・非表示を切り替えています。

これで以下のような画面になります。
非ログイン時:
image

ログイン時:
image

Automatic accounts UI

今回はパスワードを使用しましたが、accounts-facebookパッケージなどを使えば連携認証も可能です。ただし連携用のAPIキーは用意する必要があります。

$ meteor add accounts-facebook accounts-twitter               

Changes to your project's package version selections:

accounts-facebook  added, version 1.0.3       
accounts-oauth     added, version 1.1.3
accounts-twitter   added, version 1.0.3
facebook           added, version 1.1.3
oauth              added, version 1.1.3
oauth1             added, version 1.1.3
oauth2             added, version 1.1.2
twitter            added, version 1.1.3


accounts-facebook: Login service for Facebook accounts
accounts-twitter: Login service for Twitter accounts

赤色はAPIキーが未設定です。twitterを選択
image

OAuthのAPIキーを要求してきます
image

登録すると青色になりました。
image

なおCloud9上だとPopupでの連携が上手くいきませんでした。Callback先が0.0.0.0のためと思います。

Getting information about the logged-in user

html上では{{currentUser}}
js上ではMeteor.user()
を使いユーザ情報にアクセスできます。

Security with methods

現状ではクライアントサイドからDBに好きにデータを書き込めるため、セキュリティ上よろしくありません。

Removing insecure

クライアントサイドからDBをコントロールできるのはinsecureパッケージのおかげです。まずこれを削除します。

meteor remove insecure

Changes to your project's package version selections:

insecure  removed from your project           

insecure: removed dependency                  
$ 

これでクライアントサイドからDBの更新ができなくなったはずです。試しにタスクを追加してみましょう。コンソールに以下のエラーがでます。

insert failed: Access denied

Defining methods

simple-todos.jsの末尾に以下のコードを加えます。

simple-todo.js
・・・
Meteor.methods({
  addTask: function (text) {
    // Make sure the user is logged in before inserting a task
    if (! Meteor.userId()) {
      throw new Meteor.Error("not-authorized");
    }

    Tasks.insert({
      text: text,
      createdAt: new Date(),
      owner: Meteor.userId(),
      username: Meteor.user().username
    });
  },
  deleteTask: function (taskId) {
    Tasks.remove(taskId);
  },
  setChecked: function (taskId, setChecked) {
    Tasks.update(taskId, { $set: { checked: setChecked} });
  }
});

そして今まで直接DBを操作していたクライアントサイドのコードを次のように置き換えます。

// replace Tasks.insert( ... ) with:
Meteor.call("addTask", text);

// replace Tasks.update( ... ) with:
Meteor.call("setChecked", this._id, ! this.checked);

// replace Tasks.remove( ... ) with:
Meteor.call("deleteTask", this._id);

これによりクライアントサイドはサーバ側で決められた処理しか実行できなくなります。

Latency compensation

Meteor.callを使用すると、サーバサイドにajax的なリクエストを送ると同時に、クライアントサイドで処理をシミュレートします。これにより、サーバからの返信を待たずに画面に素早く反映することができます。

Filtering data with publish and subscribe

さらにセキュリティの話です。クライアントサイドはまだ好きにデータを読める状態にあります。これもよろしくありません。これはautopublish パッケージが入っているためです。削除しましょう。

$ meteor remove autopublish

Changes to your project's package version selections:

autopublish  removed from your project        

autopublish: removed dependency               
$ 

できましたね。ブラウザで画面を見てみましょう。リストが表示できないはずです。
クライアントがデータを読めるようにするにはサーバ側で読込可能なデータをpublishしてクライアント側でsubscribeする必要があります。
つまり以下のようになりますです。

simple-todos.js
・・・
// At the top of our client code
Meteor.subscribe("tasks");
・・・
// At the bottom of simple-todos.js
if (Meteor.isServer) {
  Meteor.publish("tasks", function () {
    return Tasks.find();
  });
}

これによりクライアントサイドはpublishで返されたTasksデータのみアクセスが許可されます。
次の例を見てみましょう。

Implementing private tasks

プライベートなタスクを作成する際も、上記の機能が役に立ちます。タスクにプライベート属性を追加し、それ用のトグルボタンを用意します。
まずタスクの所有者かどうか判断するisOwnerヘルパを定義をクライアント用コードの中に定義します。

simple-todos.js
  // Define a helper to check if the current user is the task owner
  Template.task.helpers({
    isOwner: function () {
      return this.owner === Meteor.userId();
    }
  });

これを使いタスクの所有者の場合のみ、プライベート用ボタンを表示するtaskテンプレートを変更します。

simple-todos.html
・・・
<template name="task">
  <!-- add right below the code for the checkbox in the task template -->
  {{#if isOwner}}
    <button class="toggle-private">
      {{#if private}}
        Private
      {{else}}
        Public
      {{/if}}
    </button>
  {{/if}}

  <!-- modify the li tag to have the private class if the item is private -->
  <li class="{{#if checked}}checked{{/if}} {{#if private}}private{{/if}}">
    <button class="delete">&times;</button>

    <input type="checkbox" checked="{{checked}}" class="toggle-checked" />

    <span class="text"><strong>{{username}}</strong> - {{text}}</span>
  </li>
</template>

次にボタンの動作を定義します。DBを更新するためMeteor.callを使用します。

  Template.task.events({
・・・
    },
    // Add an event for the new button to Template.task.events
    "click .toggle-private": function () {
      Meteor.call("setPrivate", this._id, ! this.private);
    }
  });
・・・
Meteor.methods({
・・・
  },
  // Add a method to Meteor.methods called setPrivate
  setPrivate: function (taskId, setToPrivate) {
    var task = Tasks.findOne(taskId);

    // Make sure only the task owner can make a task private
    if (task.owner !== Meteor.userId()) {
      throw new Meteor.Error("not-authorized");
    }

    Tasks.update(taskId, { $set: { private: setToPrivate } });
  }

});

最後にtasksのpublishを変更します。これによりクライアントがアクセスできるタスクが公開されているか自分が所有しているものに限定されます。

simple-todos.js
・・・
// At the bottom of simple-todos.js
if (Meteor.isServer) {
  // Modify the publish statement
  // Only publish tasks that are public or belong to the current user
  Meteor.publish("tasks", function () {
    return Tasks.find({
      $or: [
        { private: {$ne: true} },
        { owner: this.userId }
      ]
    });
  });
}

Hana:
image

Taro:
image

ではさらに攻撃者がプライベートタスクを更新・削除できないようにしましょう。これでセキュアなTodoアプリになりました。

simple-todos.js
・・・
  deleteTask: function (taskId) {
    var task = Tasks.findOne(taskId);
    if (task.private && task.owner !== Meteor.userId()) {
      // If the task is private, make sure only the owner can delete it
      throw new Meteor.Error("not-authorized");
    }
    Tasks.remove(taskId);
  },
・・・
  setChecked: function (taskId, setChecked) {
    var task = Tasks.findOne(taskId);
    if (task.private && task.owner !== Meteor.userId()) {
      // If the task is private, make sure only the owner can check it off
      throw new Meteor.Error("not-authorized");
    }
    Tasks.update(taskId, { $set: { checked: setChecked} });
  },
・・・

What's next?

サンプル集をみて勉強しましょう。

meteor create --example todos
meteor create --example localmarket

以上でチュートリアルは終わりです。

感想

MongoDBの入門にちょうどいいかなと思いました。
そのまま書くとクライアントサイドとサーバサイドがごっちゃになって逆に混乱するかな?
うまくモジュールの分割しないと保守が大変そう。
ライブアップデートがメイン要件の場合に役に立ちそうかな?

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2