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ラベルの下にでも配置してください。
<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"
});
・・・
では次にToDoを誰が作成したかわかるようにする機能を追加します。タスク登録箇所を次のようにします。
・・・
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のタスク名表示部を以下で置き換えます。
<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はユーザがログインしている場合のみ存在します。それを条件に表示・非表示を切り替えています。
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
なお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の末尾に以下のコードを加えます。
・・・
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する必要があります。
つまり以下のようになりますです。
・・・
// 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ヘルパを定義をクライアント用コードの中に定義します。
// Define a helper to check if the current user is the task owner
Template.task.helpers({
isOwner: function () {
return this.owner === Meteor.userId();
}
});
これを使いタスクの所有者の場合のみ、プライベート用ボタンを表示するtaskテンプレートを変更します。
・・・
<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">×</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を変更します。これによりクライアントがアクセスできるタスクが公開されているか自分が所有しているものに限定されます。
・・・
// 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 }
]
});
});
}
ではさらに攻撃者がプライベートタスクを更新・削除できないようにしましょう。これでセキュアなTodoアプリになりました。
・・・
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の入門にちょうどいいかなと思いました。
そのまま書くとクライアントサイドとサーバサイドがごっちゃになって逆に混乱するかな?
うまくモジュールの分割しないと保守が大変そう。
ライブアップデートがメイン要件の場合に役に立ちそうかな?