プログラミングとSEOと暇つぶし

駆け出しエンジニアdallPのブログです。元SEOコンサルタントです。プログラミング、SEO、アフィリエイト、お金などについて役に立つかもしれない情報をやりたいように書きます。

MeteorJSとReactを勉強してみる その9: メソッドでセキュリティを強化する【公式翻訳】

あけましておめでとうございます。 本当は去年のうちに終わらせておくつもりだったのですが、仕事が立て込んだので2018年に食い込んでしまいました・・

MeteorJSチュートリアルの第9弾、今回はアプリのセキュリティ強化のお話です。


メソッドでセキュリティを強化する

このステップの実装を行う前では、作成中のアプリのあらゆるデータベースを誰でも編集できてしまいます。

組織内部向けの小さなアプリケーションやデモのアプリであれば良いでしょうが、不特定多数に公開される本来のアプリケーションであれば、データへのアクセスにはパーミッションのコントロールが必須です。

Meteorでは、パーミッションコントロールを行う最善の方法は関数を宣言することによるものです。

クライアント側のコードで直接insertupdateremoveなどを呼ぶのではなく、ユーザーがそれらのアクションを行う権限を持っているかをチェックし、クライアントの代わりにデータベースに変更を加えるようにします。

insecureを外す

新しく作成されたMeteorのプロジェクトは、デフォルトでinsecureパッケージが追加されています。 これはクライアント側からデータベースを編集できるようにするためのパッケージです。

insecureはプロトタイピングの段階では便利ですが、もはや補助輪を外す段階に来ています。

このパッケージを外すために、ターミナルでアプリのディレクトリに移動し、下記のコマンドを打ちましょう。

meteor remove insecure

insecureパッケージを除外した後でアプリを使おうとすると、全ての入力欄やボタンが動かなくなっていることが分かるでしょう。

これは、クライアントサイドのデータベース編集パーミッションが無効化されたためです。

この状態からアプリを動かすには、いくつかコードを書き換える必要があります。

メソッドを定義する

まず、いくつかのメソッドを定義する必要があります。 クライアント上で動かしたいデータベース操作それぞれについて、一つのメソッドが必要です。

メソッドはクライアントとサーバーで実行されているコードの中で定義される必要があります。 (これについては、後ほど「Optimistic UI」という段落で少々解説します)

imports/api/tasks.js

before

import { Mongo } from 'meteor/mongo';

export const Tasks = new Mongo.Collection('tasks');

after

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 } });
  }
});

メソッドを定義し終わったので、これまでのコードでコレクションに対して直接操作を行っていた部分について、上記メソッドを利用するように書き換えましょう。

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();

    Tasks.insert({
      text,
      createdAt: new Date(),
      owner: Meteor.userId(),
      username: Meteor.user().username
    });

    // 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(() => {
  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 { 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
    Tasks.update(this.props.task._id, {
      $set: { checked: !this.props.task.checked },
    })
  };

  deleteThisTask() {
    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) }>
          &times;
        </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) }>
          &times;
        </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>
    );
  }
}

これで全ての入力欄とボタンがまた動くようになりました。 これにより何が良くなったのかというと、

  1. データベースにタスクを入力する際、ユーザーがログインしており、createdAtが正しく、ownerとusernameが正しく、ユーザーが他の誰かを偽装しているわけではない、ということを確認できる
  2. 後のステップでタスクのプライベート化を行う際に、setCheckeddeleteTaskに更なるバリデーションを追加できる
  3. クライアントコードがデータベースのロジックと分割されており、イベントハンドラの中で様々な処理が行われる状況を避け、どこからでも呼び出せる共通メソッドを書くことができた

ことが挙げられます。

Optimistic UI

ここで、なぜクライアント側とサーバー側でメソッドを定義したいのかを確認しましょう。 それは、我々が「optimistic UI」と呼ぶ特徴を利用したいためです。

Meteor.callを使ってクライアント側でメソッドを呼び出す際、並行して二つの処理が行われています。

  1. AJAXリクエストの動き方と同じような感じで、クライアントがサーバーにセキュアな環境でメソッドを動かすようにリクエストを送る
  2. 利用可能な情報から、サーバーにより出力されるであろう結果を予測するためにクライアント上でメソッドのシミュレーションが実行される

これはつまり、サーバーから結果が出力される前に、スクリーン上には新しく作成されたタスクが表示されるということです。

サーバーから結果が返却され、その結果がクライアントのシミュレーションと矛盾しなければ、シミュレーションによる結果はそのまま残ります。

仮に返却値がシミュレーションと異なった場合は、クライアント側の方がサーバーの実際の状態と合致するように修正されます。

メソッドとoptimistic UIについては、Meteor Guideのメソッドの記事及び、optimistic UIに関するブログ記事で詳しく知ることができます。


いかがでしょうか。

初心者の方の場合、これでなぜセキュリティが強化されているのかよくわからない、ということもあるかと思いますが、とにかくまずはクライアント側とサーバー側で役割分担すべき、ということは覚えておくと良いかと思います。

MeteorJS(1.6.0.1) with React 公式チュートリアル 日本語訳版まとめ

Javascriptフルスタックフレームワーク、Meteorのチュートリアルを勉強がてら翻訳しました。

Meteorのバージョンは1.6.0.1です。

フロントのフレームワークがMeteor独自のBlaze、React、Angularから選べるのですが、個人的な意向でReactのやつを翻訳してます。

未翻訳のものは出来次第リンクを付け足します。

チュートリアル

  1. MeteorJSを勉強してみる その1: 初めてのアプリを作ろう【公式翻訳】

  2. MeteorJSを勉強してみる その2: ReactコンポーネントでViewを作成しよう【公式翻訳】

  3. MeteorJSを勉強してみる その3: コレクションにタスクのデータを蓄積しよう【公式翻訳】

  4. MeteorJSを勉強してみる その4: フォームからタスクを追加してみよう【公式翻訳】

  5. MeteorJSを勉強してみる その5: タスクの完了と削除を実装してみよう【公式翻訳】

  6. MeteorJSを勉強してみる その6: iOSとAndroidでアプリを動かしてみよう【公式翻訳】

  7. MeteorJSを勉強してみる その7: コンポーネントのステートにUIの状態を保存してみよう【公式翻訳】

  8. MeteorJSを勉強してみる その8: ユーザーアカウント機能を追加してみよう【公式翻訳】

  9. MeteorJSとReactを勉強してみる その9: メソッドでセキュリティを強化する【公式翻訳】

  10. MeteorJSとReactを勉強してみる その10: publishとsubscribeで表示するデータをフィルタリングしよう【公式翻訳】

  11. MeteorJSとReactを勉強してみる その11: テストコードを書いてみよう【公式翻訳】

その他関連記事翻訳

MeteorJSのMongoDBポートが分からずDBのGUIツールの設定に躓いた話

チュートリアル終わったのであとは気ままに更新していきます

MeteorJSとReactを勉強してみる その8: ユーザーアカウント機能を追加してみよう【公式翻訳】

MeteorJSを勉強してみるシリーズ第八弾です。

やっとユーザーアカウントを追加する段階に来ましたね。 しかもMeteorではユーザーアカウント機能をデフォルトで用意してくれているため、一から自分で作るよりも圧倒的に簡単に機能を追加できます。


ユーザーアカウント機能を追加する

Meteorはアカウント機能とログインユーザーインターフェースを備えており、マルチユーザー機能をアプリに追加することも簡単にできてしまいます。

なお現状ではこのUIコンポーネントはMeteor独自のUIエンジンである「Blaze」を使用しています。Reactを使ったコンポーネントはまだ用意されていませんが、将来的には実装されるかもしれません。

アカウント機能とそのUIを使えるようにするには、関連するパッケージをプロジェクトに追加しなくてはなりません。

アプリのディレクトリに行き、下記のコマンドを打ちましょう。

meteor add accounts-ui accounts-password

BlazeコンポーネントをReactで包む

Blazeのaccounts-uiUIコンポーネントをReactで使うには、Reactコンポーネントでラップする必要があります。

そのためには、まずAccountsUIWrapperというReactコンポーネントを新しいファイルで作ってあげましょう。

imports/ui/AccountsUIWrapper.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
 
export default class AccountsUIWrapper extends Component {
  componentDidMount() {
    // Use Meteor Blaze to render login buttons
    this.view = Blaze.render(Template.loginButtons,
      ReactDOM.findDOMNode(this.refs.container));
  }
  componentWillUnmount() {
    // Clean up Blaze view
    Blaze.remove(this.view);
  }
  render() {
    // Just render a placeholder container that will be filled in
    return <span ref="container" />;
  }
}

今作成したこのコンポーネントをApp.jsで読み込んであげましょう。

imports/ui/App.js

before

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.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();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // 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>

          <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(),
  };
})(App);

after

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
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();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // 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 />

          <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(),
  };
})(App);

Then, add the following code to configure the accounts UI to use usernames instead of email addresses:

さらに、imports配下にstartupディレクトリを作成し、その中にaccounts-config.jsを作成しましょう。 これはaccounts-uiのログイン時にメールアドレスではなくユーザーネームを使用するようにするための設定です。

imports/startup/accounts-config.js

import { Accounts } from 'meteor/accounts-base';
 
Accounts.ui.config({
  passwordSignupFields: 'USERNAME_ONLY',
});

We also need to import that configuration code in our client side entrypoint:

この設定コードはクライアント側のエントリーポイント(client/main.js)で読み込んであげましょう。

client/main.js

before

import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';

import App from '../imports/ui/App.js';

Meteor.startup(() => {
  render(<App />, document.getElementById('render-target'));
});

after

import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';

import '../imports/startup/accounts-config.js';
import App from '../imports/ui/App.js';

Meteor.startup(() => {
  render(<App />, document.getElementById('render-target'));
});

ユーザーに関連する機能を追加する

これでユーザーはアカウントを作成し、アプリにログイン出来るようになりました。 とても素晴らしいのですが、ログインできるだけでは便利じゃありませんね。 ということで、二つ新しい機能を追加しましょう。

  • ログイン中のユーザーにのみタスク入力欄を表示する
  • どのユーザーがどのタスクを作ったのかを表示する

これらを実装するため、タスクコレクションに二つの新しいフィールドを持たせましょう。

  1. owner: タスクを作ったユーザーのid
  2. username: タスクを作ったユーザーのユーザーネーム

ここでは、タスクを表示するたびに毎回ユーザーを探しにいかなくても良いように、タスクオブジェクトに直接ユーザーネームを保存します。

まず、タスク追加時に上記のフィールドを保存するために、App.jshandleSubmitイベントハンドラに追記しましょう。

現在ログインしているユーザーの情報を取得するため、データコンテナにも追記します。

そして入力フォームを表示するのはログインしているユーザーだけ、という条件を付け加えましょう。

imports/ui/App.js

before

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
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();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // 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 />

          <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()
  };
})(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();

    Tasks.insert({
      text,
      createdAt: new Date(),
      owner: Meteor.userId(),
      username: Meteor.user().username
    });

    // 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);

最後に、各タスクの左側に、そのタスクを追加したユーザーのユーザーネームを表示するためのコードを追記しましょう。

imports/ui/Task.js

before

import React, { Component } from 'react';

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
    Tasks.update(this.props.task._id, {
      $set: { checked: !this.props.task.checked },
    })
  };

  deleteThisTask() {
    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) }>
          &times;
        </button>

        <input
          type="checkbox"
          readOnly
          checked={ !!this.props.task.checked }
          onClick={ this.toggleChecked.bind(this) }
        />

        <span className="text">{ this.props.task.text }</span>
      </li>
    );
  }
}

after

import React, { Component } from 'react';

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
    Tasks.update(this.props.task._id, {
      $set: { checked: !this.props.task.checked },
    })
  };

  deleteThisTask() {
    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) }>
          &times;
        </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>
    );
  }
}

ブラウザで、まずは自分のアカウントを作成してください。 その上でログインすると、タスク入力欄が表示されると思います。 その状態でタスクを追加すると、自分のユーザーネームと一緒にタスクが表示されるようになっているでしょうか。

以前までに追加したタスクにはユーザーネームがついていませんね。

さて、ユーザーはログインできるようになり、各タスクがどのユーザーに紐付いているのか分かるようになりました。

本ステップで初めて出てきたMeteorの特徴について、もう少し詳しく見ておきましょう。

自動化されたアカウントUI

Meteorで作成中のアプリにaccounts-uiパッケージが含まれているなら、ログインメニューを作成するのに必要なのはUIコンポーネントレンダリングすることだけです。

このログイン機能は、どのログインメソッドがアプリに追加されているかを自動で検知し、最適なUIを表示します。

今回のケースでは、利用可能になっているメソッドはaccounts-passwordのみですので、ドロップダウンメニューにはパスワード入力欄しかありません。

もし他の機能も試してみたければ、accounts-facebookパッケージを追加して、Facebookログインボタンを有効化することもできます。

ログイン中ユーザーの情報取得について

データコンテナでは、Meteor.user()を使ってユーザーがログイン中かどうかを判定し、彼らの情報を取得することができます。 例えば、Meteor.user().usernameはログインユーザーのユーザーネームを持っています。 同様に、Meteor.userId()ではログインユーザーのユーザーIDを取得できます。

次のステップでは、サーバーでデータの検証を行うことで作成中のアプリをセキュアにする方法を学んでいきます。


非常に簡単にユーザーアカウント機能を追加することができましたね。

めちゃくちゃ簡単なのは良い点も悪い点もあり、良い点としては単純に実装が早くなること、悪い点は最悪何も理解してなくても実装できてしまうこと、既存の実装をうまいこと改変することができない or 難しいと想定されることです。

でも、とりあえず作りたいものがある、という場合にはやっぱり非常にありがたい用意ですよね。

ついでにですが、Facebookログイン機能は少なくともローカルでアプリを作成中の段階では有効化できなさそうです。 これはMeteorの問題というより、Facebook側のバリデーションの関係で無理な気がしています。

抜け道があるのかどうかまでは調べていないので、どうしてもローカルホストで試したいんだ、という人はググってみてくださいね。

次の更新は明日、もしくは週明けくらいになると思います。