MeteorJSとReactを勉強してみる その9: メソッドでセキュリティを強化する【公式翻訳】
あけましておめでとうございます。 本当は去年のうちに終わらせておくつもりだったのですが、仕事が立て込んだので2018年に食い込んでしまいました・・
MeteorJSチュートリアルの第9弾、今回はアプリのセキュリティ強化のお話です。
メソッドでセキュリティを強化する
このステップの実装を行う前では、作成中のアプリのあらゆるデータベースを誰でも編集できてしまいます。
組織内部向けの小さなアプリケーションやデモのアプリであれば良いでしょうが、不特定多数に公開される本来のアプリケーションであれば、データへのアクセスにはパーミッションのコントロールが必須です。
Meteorでは、パーミッションコントロールを行う最善の方法は関数を宣言することによるものです。
クライアント側のコードで直接insert
、update
、remove
などを呼ぶのではなく、ユーザーがそれらのアクションを行う権限を持っているかをチェックし、クライアントの代わりにデータベースに変更を加えるようにします。
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) }> × </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) } /> <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
これで全ての入力欄とボタンがまた動くようになりました。 これにより何が良くなったのかというと、
- データベースにタスクを入力する際、ユーザーがログインしており、createdAtが正しく、ownerとusernameが正しく、ユーザーが他の誰かを偽装しているわけではない、ということを確認できる
- 後のステップでタスクのプライベート化を行う際に、
setChecked
とdeleteTask
に更なるバリデーションを追加できる - クライアントコードがデータベースのロジックと分割されており、イベントハンドラの中で様々な処理が行われる状況を避け、どこからでも呼び出せる共通メソッドを書くことができた
ことが挙げられます。
Optimistic UI
ここで、なぜクライアント側とサーバー側でメソッドを定義したいのかを確認しましょう。 それは、我々が「optimistic UI」と呼ぶ特徴を利用したいためです。
Meteor.call
を使ってクライアント側でメソッドを呼び出す際、並行して二つの処理が行われています。
- AJAXリクエストの動き方と同じような感じで、クライアントがサーバーにセキュアな環境でメソッドを動かすようにリクエストを送る
- 利用可能な情報から、サーバーにより出力されるであろう結果を予測するためにクライアント上でメソッドのシミュレーションが実行される
これはつまり、サーバーから結果が出力される前に、スクリーン上には新しく作成されたタスクが表示されるということです。
サーバーから結果が返却され、その結果がクライアントのシミュレーションと矛盾しなければ、シミュレーションによる結果はそのまま残ります。
仮に返却値がシミュレーションと異なった場合は、クライアント側の方がサーバーの実際の状態と合致するように修正されます。
メソッドと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のやつを翻訳してます。
未翻訳のものは出来次第リンクを付け足します。
チュートリアル
その他関連記事翻訳
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-ui
UIコンポーネントを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')); });
ユーザーに関連する機能を追加する
これでユーザーはアカウントを作成し、アプリにログイン出来るようになりました。 とても素晴らしいのですが、ログインできるだけでは便利じゃありませんね。 ということで、二つ新しい機能を追加しましょう。
- ログイン中のユーザーにのみタスク入力欄を表示する
- どのユーザーがどのタスクを作ったのかを表示する
これらを実装するため、タスクコレクションに二つの新しいフィールドを持たせましょう。
- owner: タスクを作ったユーザーのid
- username: タスクを作ったユーザーのユーザーネーム
ここでは、タスクを表示するたびに毎回ユーザーを探しにいかなくても良いように、タスクオブジェクトに直接ユーザーネームを保存します。
まず、タスク追加時に上記のフィールドを保存するために、App.js
のhandleSubmit
イベントハンドラに追記しましょう。
現在ログインしているユーザーの情報を取得するため、データコンテナにも追記します。
そして入力フォームを表示するのはログインしているユーザーだけ、という条件を付け加えましょう。
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) }> × </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) }> × </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側のバリデーションの関係で無理な気がしています。
抜け道があるのかどうかまでは調べていないので、どうしてもローカルホストで試したいんだ、という人はググってみてくださいね。
次の更新は明日、もしくは週明けくらいになると思います。