MeteorJSとReactを勉強してみる その10: publishとsubscribeで表示するデータをフィルタリングしよう【公式翻訳】
MeteorJSを勉強してみるシリーズ第10回です。
チュートリアルとしての実装は今回で終了となります。
publishとsubscribeでデータをフィルタリングする
アプリのセンシティブなコードは全てメソッドに移したので、これからMeteorのセキュリティの後半を学習しましょう。
ここまでは、データベース全体がクライアントに表示される想定でコーディングしてきました。
これはつまり、Tasks.find()
するとコレクション内の全てのタスクが表示されてしまうということです。
しかし、もしユーザーがプライベートなデータを保存しておきたいとしたらこれはよくありません。 Meteorがどのデータをクライアントに送るのかをコントロールする方法が必要です。
前回のステップのinsecure
と同様に、Meteorアプリはデフォルトでautopublish
パッケージを含みます。
これはデータベースの情報を全てクライアントに自動的に同期させる機能です。
autopublish
を外すとどうなるか見てみましょう。
meteor remove autopublish
アプリが更新されると、タスクリストが空になっていると思います。 autopublishパッケージが無い状態だと、サーバーからクライアントになんのデータを送るのか、明確に示さないとなりません。
Meteorでそれを行う関数として、Meteor.publish
とMeteor.subscribe
が用意されています。
まずは、全てのタスクにpublicationを追加しましょう。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); } });
次に、Appコンポーネントができた時点でこのpublicationをsubscribeしましょう。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
これらのコードを追加すると、全タスクが再度表示されるでしょう。
Meteor.publish
をサーバーで呼ぶと、「tasks」という名前のpublicationが登録されます。
Meteor.subscribe
をクライアントでpublicationの名前付きで呼ぶと、クライアントはそのpublicationから全てのデータをsubscribeします。
つまり今回のケースでは、データベース内にある全てのtasksデータです。
publish/subsribeモデルの真価を確認するために、ユーザーがタスクを「プライベート」化し、他のユーザーには見えなくする機能を開発してみましょう。
タスクをプライベート化するボタンを追加する
tasksに新しい「private」というプロパティと、ユーザーがタスクをプライベート化できるボタンを作成しましょう。 このボタンはそのタスクのownerにしか表示されるべきではありません。 さらに、タスクがpublicかprivateか、どちらの状態なのかを示すラベルも追加しましょう。
まず、タスクのprivateステータスをセットできるメソッドを追加します。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
さて、表示されているタスクにprivateボタンを表示するかどうかを判定するために、タスクには新しいプロパティを渡しておかなくてはいけません。 ボタンは、現在ログイン中のユーザーがそのタスクのownerである場合のみ、表示されるべきです。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( const currentUserId = this.props.currentUser && this.props.currentUser._id; const showPrivateButton = task.owner === currentUserId; <Task key={task._id} task={task} showPrivateButton={showPrivateButton} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
ボタンを追加し、この新しく追加されたプロパティを使って表示可否を判断するようにしましょう。
imports/ui/Task.js
before
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
after
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
ボタン押下時に呼ばれるイベントハンドラも定義しておきましょう。
imports/ui/Task.js
before
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
after
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; togglePrivate() { Method.call('tasks.setPrivate', this.props.task._id, !this.props.task.private); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
最後に、Taskコンポーネント内の
classnames
のnpmパッケージを使います。
meteor npm install --save classnames
タスクのレンダリング中にクラスが選択されるようにパッケージを使ってみましょう。
imports/ui/Task.js
before
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; togglePrivate() { Method.call('tasks.setPrivate', this.props.task._id, !this.props.task.private); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
after
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import classnames from 'classnames'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; togglePrivate() { Method.call('tasks.setPrivate', this.props.task._id, !this.props.task.private); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = classnames({ checked: this.props.task.checked, private: this.props.task.private }); return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
privacyステータスに応じてタスクを選択して表示する
タスクをプライベートにすることができるようになったので、publication関数で各ユーザーが閲覧権限を持つタスクだけを出力するように変更しましょう。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server // Only publish tasks that are public or belong to the current user Meteor.publish('tasks', function tasksPublication() { return Tasks.find({ $or: [ { private: { $ne: true } }, { owner: this.userId } ] }); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
この機能が動いていることを確かめるためには、ブラウザーのプライベートブラウジングモード(Chromeならシークレットブラウザー)を使って別ユーザーとしてログインしてみればよいでしょう。
2つのウィンドウを隣り合わせに配置して、他のユーザーが閲覧できないようにタスクをプライベート化してみて下さい。 もう一度パブリックにすると、再表示されるはずです!
セキュリティ機能を追加する
プライベートタスク機能を完成させるため、deleteTask
とsetChecked
にチェック機能をつけましょう。
これでタスクのownerのみがプライベートタスクを削除したり、チェックを外したり出来るようになります。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server // Only publish tasks that are public or belong to the current user Meteor.publish('tasks', function tasksPublication() { return Tasks.find({ $or: [ { private: { $ne: true } }, { owner: this.userId } ] }); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server // Only publish tasks that are public or belong to the current user Meteor.publish('tasks', function tasksPublication() { return Tasks.find({ $or: [ { private: { $ne: true } }, { owner: this.userId } ] }); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); const task = Tasks.findOne(taskId); if (task.private && task.owner !== this.userId) { // If the task is private, make sure only the owner can delete it throw new Meteor.Error('not-authorized'); } Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); const task = Tasks.findOne(taskId); if (task.private && task.owner !== this.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 } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
ちなみに、このままではパブリックなタスクであればownerが誰であろうと、誰でもタスクを削除できてしまいます。 ownerのみがタスクを削除出来るようにするためには簡単な修正を加えれば良いだけですので、試してみても良いでしょう。
これでプライベートタスク機能は完成です! 攻撃者が誰かのプライベートタスクを覗いたり修正したりすることを防げるようになりました。
第10回の内容は以上です。
Meteorのチュートリアルも残すところあと1つとなりました。
見てくださっている方がもしいましたら、是非最後までお付き合いくださいm(. .)m