MeteorはWebアプリを超高速に作れるプラットフォームで、いろいろ便利な機能を提供してくれているけど、その中でもユーザアカウント機能は本当に超簡単に追加することができます。accounts-password と accounts-uiの2つのパッケージをaddするだけで、ユーザ登録とログイン認証機能がすぐに実装できます。UI込みでです。(但しreactを使う時は少しのコーディングが必要ですが。)
今回はaccounts-uiを使わずに、React + React-router + Material-ui でカスタマイズ画面を作って、以下の機能を実装しましたのでその報告です。accounts-passwordの機能だけを使っています。accounts-uiを使う場合はあまりにも簡単なのですが、使わない場合の例があまり見当たらないので、備忘録も兼ての報告です。
- ユーザ登録(メール確認無し)
- ユーザ登録(メール確認あり)
- パスワード再発行
- ログイン
- ログアウト
- パスワード変更
実際にはマルチユーザ対応のカレンダーアプリを作りながら試しましたが、まずは以下のパッケージをインストールします。(カレンダーアプリについては公開できる時が来たら、またその報告を書きたいと思います。)
$ meteor create mycalendar2
$ cd mycalendar2/
$ meteor npm install --save react react-dom prop-types react-router react-router-dom
$ meteor npm install --save material-ui react-tap-event-plugin formsy-react
$ meteor npm install --save react-big-calendar moment
$ meteor add react-meteor-data
$ meteor add accounts-password
まずnpmパッケージについてですが、meteor上でreactを使ってログイン/ユーザ登録等の複数の画面作りを行いますので、reactやreact-router関連のパッケージを入れます。デザインはMaterial-UIに任せます。Formのバリデーションにformsy-reactを使います。カレンダー表示のために react-big-calendar+momentを使います。
次にmeteorパッケージについてです。meteorとreactの連携のためにreact-meteor-dataを使います。meteorのアプリですのでデータ管理については、今のところreduxは使わず、minimongoのみを使う予定です。アカウント関係についてですが、accounts-passwordをインストールすると、accounts-baseも同時にインストールされます。今回の目的はカスタマイズUIを作ることなので、accounts-uiは使いません。
1.ユーザ登録(メール確認無し)
まず、ユーザテーブルについてですが、usernameとemail、passwordの3つの項目を持たせることにします。これは本文を通して変わりません。またreact画面については通常2つのcomponentを作って実装します。imports/ui/viewsにcontainerのcomponentを置きます。imports/ui/componentsに具体的な画面を定義するcomponentを置きます。
メール確認なしのユーザ登録の場合は、ユーザ登録画面のcomponentでusernameとemail、passwordを入力させ、componentのイベントハンドラーでユーザ登録を行います。
ここのポイントはAccounts.createUser()を使って、クライアントから直接ユーザ登録を行っている点です。サーバ側のコードは必要ありません。これで登録したメールアドレスとパスワードでログインできるようになります。下位のRegisterFormのコードも示しておきます。
import React from 'react';
import Formsy from 'formsy-react';
import { RaisedButton, Paper } from 'material-ui';
import DefaultInput from './DefaultInput';
export default class RegisterForm extends React.Component {
constructor(props) {
super(props);
this.enableButton = this.enableButton.bind(this);
this.disableButton = this.disableButton.bind(this);
this.state = {
canSubmit: false
};
}
enableButton() {
this.setState({
canSubmit: true
});
}
disableButton() {
this.setState({
canSubmit: false
});
}
render() {
return (
<Formsy.Form onSubmit={this.props.onSubmit} onValid={this.enableButton} onInvalid={this.disableButton}>
<Paper zDepth={1} style={{padding: 32}}>
<h3>ユーザ登録フォーム</h3>
<DefaultInput name='username' title='ユーザ名' value="" required />
<DefaultInput name='email' validations='isEmail' validationError='This is not a valid email' title='メール' value="" required />
<DefaultInput type='password' name='password' title='パスワード' value="" required />
<div style={{marginTop: 24}}>
<RaisedButton
disabled={!this.state.canSubmit}
secondary={true}
type="submit"
style={{margin: '0 auto', display: 'block', width: 150}}
label={'登録'} />
</div>
</Paper>
</Formsy.Form>
);
}
}
2.ユーザ登録(メール確認あり)
ユーザ登録時にメール確認を必要とする場合です。今回は、メール送信を行いますので、サーバ側のコードを必要とします。ユーザ登録画面にはパスワード入力欄の必要はありません。以下にコードを示します
import React from 'react';
import RegisterForm from '../components/RegisterForm.js';
import { handleErrorFunc } from '../App';
export default class RegisterView extends React.Component {
constructor(props) {
super(props);
this.register = this.register.bind(this);
this.state = {
error: null
};
}
register (newUser) {
{"username":"taro","email":"xxxxx@gmail.com"}
Meteor.call('createNewUser', newUser, (err,result) => {
if (err) {
handleErrorFunc(err.message);
} else {
this.props.history.push({pathname: '/login'});
}
});
}
render () {
return (
<div>
<h1>登録画面</h1>
<div style={{maxWidth: 450, margin: '0 auto'}}>
<RegisterForm
onSubmit={this.register} />
</div>
</div>
);
}
}
登録のハンドラーはMeteor.call('createNewUser', newUser, ...)でサーバ側のコードを呼ぶ点が前回と大きく異なります。newUserにはpassword情報はありません。下位のcomponentはpassword欄が無い点のみが異なります。
import React from 'react';
import Formsy from 'formsy-react';
import { RaisedButton, Paper } from 'material-ui';
import DefaultInput from './DefaultInput';
export default class RegisterForm extends React.Component {
constructor(props) {
super(props);
this.enableButton = this.enableButton.bind(this);
this.disableButton = this.disableButton.bind(this);
this.state = {
canSubmit: false
};
}
enableButton() {
this.setState({
canSubmit: true
});
}
disableButton() {
this.setState({
canSubmit: false
});
}
render() {
return (
<Formsy.Form onSubmit={this.props.onSubmit} onValid={this.enableButton} onInvalid={this.disableButton}>
<Paper zDepth={1} style={{padding: 32}}>
<h3>ユーザ登録フォーム</h3>
<DefaultInput name='username' title='ユーザ名' value="" required />
<DefaultInput name='email' validations='isEmail' validationError='This is not a valid email' title='メール' value="" required />
<div style={{marginTop: 24}}>
<RaisedButton
disabled={!this.state.canSubmit}
secondary={true}
type="submit"
style={{margin: '0 auto', display: 'block', width: 150}}
label={'登録'} />
</div>
</Paper>
</Formsy.Form>
);
}
}
サーバ側ですが、server/main.jsでメール内容のカスタマイズを行い、imports/api/events.jsでmethodのサーバ側コードを書いておきます。
import { Meteor } from 'meteor/meteor';
import '../imports/api/events.js';
Meteor.startup(() => {
// code to run on server at startup
Accounts.emailTemplates.siteName = "マイカレンダー";
Accounts.emailTemplates.from = "マイカレンダー <no-reply@mypress.jp>";
Accounts.emailTemplates.enrollAccount = {
subject(user) {
return "(マイカレンダー)パスワード設定";
},
text(user, url) {
url = url.replace('\#\/enroll-account', 'reset2-password')
const mess = `ユーザ登録が成功しました。
以下のリンクをクリックするとパスワードリセット画面に飛びます。
(注意)このメールに返信は行わないようお願いします。
`; // end
return(mess+url);
}
};
});
meteorのAPIであるAccounts.emailTemplatesを使うことによって、メール内容をカスタマイズしています。公式サイトをご確認ください。
http://docs.meteor.com/api/passwords.html
ここでの注意点は以下のコードです。
url = url.replace('\#\/enroll-account', 'reset2-password')
メールには、urlが含まれ、クリックするとパスワードリセット画面に飛ぶようになっています。urlにはtokenが含まれておりリセット画面で安全に自分のパスワードを設定することが可能となっています。ここではurlを自分のアプリ画面(routerで指定しているcomponent)に飛ぶように変更しています。以下にurl置換の例を示します。末尾のいかにも暗号みたいな文字列がtokenです。
【置換前】http://www.mypress.jp:3000/#/enroll-account/T9EH-_B52wKgci-_LrljWjEk1yJdj0LF5D14q88pw3o
【置換後】http://www.mypress.jp:3000/reset2-password/T9EH-_B52wKgci-_LrljWjEk1yJdj0LF5D14q88pw3o
次にサーバ側のmethodです。
if (Meteor.isServer) {
Meteor.methods({
createNewUser: function (newUser) {
const current_pass = newUser.username + (Math.floor( Math.random() * 10001 ) + 12345);
const userId = Accounts.createUser({username: newUser.username, email: newUser.email, password: current_pass});
Accounts.sendEnrollmentEmail(userId);
}
});
}
前回はクライアント側で呼んでいたAccounts.createUser()を、今回はサーバ側で呼んでいます。大きく異なるのは、パスワードは適当に乱数で生成している点です。このパスワードは後で、パスワードリセット画面で上書きされますので、使われることのないものです。最後にAccounts.sendEnrollmentEmail(userId)でメールを送信します。内容はmain.jsで設定したものとなります。メールに含まれるurlをクリックするとパスワードリセット画面に飛びます。
import React from 'react';
import ResetPasswordForm from '../components/ResetPasswordForm.js';
import { userIdFunc, handleErrorFunc } from '../App';
export default class ResetPasswordView extends React.Component {
constructor(props) {
super(props);
this.resetPassword = this.resetPassword.bind(this);
}
resetPassword(cred) {
const token = this.props.match.params.token;
Accounts.resetPassword(token, cred.password, (err) => {
if (err) {
handleErrorFunc(err.message); //親コンポネントにエラーを表示する
} else {
userIdFunc(); //親コンポネントのメニュー表示を変更する
this.props.history.push({pathname: '/'});
}
});
}
render () {
return (
<div>
<h1>パスワードリセット画面</h1>
<div style={{maxWidth: 450, margin: '0 auto'}}>
<ResetPasswordForm
onSubmit={this.resetPassword} />
</div>
</div>
);
}
}
ここではAccounts.resetPassword(token, cred.password,...)を使ってパスワードをリセットします。tokenがその権利を保障してくれます。 リセット画面はreact-routerで以下のように定義されていますので、this.props.match.params.tokenで取り出すことができます。
<Route path='/reset2-password/:token' component={ResetPasswordView} name='reset' />
最後にmeteor上でのメール送信には以下の環境変数の設定が必要となります。嵌りどころなので重要です。
export MAIL_URL='smtp://localhost:25'
export ROOT_URL='http://www.mypress.jp:3000/'
3.パスワード再発行
パスワード再発行は、再発行画面でメールを入力させ、そのメールに前回と同じパスワードリセット画面へのurlを含めます。リセット画面でパスワードを上書きさせることで再発行とします。
以下がパスワード再発行画面のcomponentです。下位のRecoverFormは他と同じですので省略します。
import React from 'react';
import RecoverForm from '../components/RecoverForm.js';
import { userIdFunc, handleErrorFunc } from '../App';
export default class RecoverView extends React.Component {
constructor(props) {
super(props);
this.recover = this.recover.bind(this);
}
recover(cred) {
Accounts.forgotPassword({ email: cred.email }, (err) => {
if (err) {
handleErrorFunc(err.message);
} else {
userIdFunc();
this.props.history.push({pathname: '/'});
}
});
}
render () {
return (
<div>
<h1>パスワード再発行画面</h1>
<div style={{maxWidth: 450, margin: '0 auto'}}>
<RecoverForm
onSubmit={this.recover} />
</div>
</div>
);
}
}
Accounts.forgotPassword()は、以下のserver.jsのカスタマイズに従って、入力されたメールアドレス宛にメールを送信します。
import { Meteor } from 'meteor/meteor';
import '../imports/api/events.js';
Meteor.startup(() => {
// code to run on server at startup
Accounts.emailTemplates.siteName = "マイカレンダー";
Accounts.emailTemplates.from = "マイカレンダー <no-reply@mypress.jp>";
Accounts.emailTemplates.resetPassword = {
subject(user) {
return "(マイカレンダー)パスワードリセット";
},
text(user, url) {
url = url.replace('\#\/reset', 'reset2')
const mess = `以下のリンクをクリックするとパスワードリセット画面に飛びます。
(注意)このメールに返信は行わないようお願いします。
`; // end
return(mess+url);
}
};
});
前回はAccounts.emailTemplates.enrollAccountでしたが、今回はAccounts.emailTemplates.resetPasswordをカスタマイズします。内容は同じで、パスワードリセット画面に飛ばすためのurlを含みます。安心してください。tokenもついています。
4.ログイン
ログインは今までと同じようなcomponentを2つ作るだけです。下位componentは今までと同じようですので省略します。
import React from 'react';
import LoginForm from '../components/LoginForm.js';
import { userIdFunc, handleErrorFunc } from '../App';
export default class LoginView extends React.Component {
constructor(props) {
super(props);
this.login = this.login.bind(this);
}
login(cred) {
Meteor.loginWithPassword(cred.email, cred.password, (err) => {
if (err) {
handleErrorFunc(err.message);
} else {
userIdFunc();
this.props.history.push({pathname: '/calendar'});
}
});
}
render () {
return (
<div>
<h1>ログイン画面</h1>
<div style={{maxWidth: 450, margin: '0 auto'}}>
<LoginForm
onSubmit={this.login} />
</div>
</div>
);
}
}
ここで使うAPIはMeteor.loginWithPassword(cred.email, cred.password,...)です。
5.ログアウト
ログアウトはログインすると、ログイン・メニューに代わり表示されるります。リンクをクリックされるとcomponentDidMountの中でMeteor.logout()を呼んですぐにログアウトするようにしています。
"use strict";
import React from 'react';
import { Link } from 'react-router-dom'
import { Paper } from 'material-ui';
import { userIdFunc, handleErrorFunc } from '../App';
class LogoutView extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
Meteor.logout( (err) => {
if (err) {
handleErrorFunc(err.message);
} else {
userIdFunc();
}
});
}
render () {
return (
<div style={{width: 400, margin: "auto"}}>
<Paper zDepth={3} style={{padding: 32, margin: 32}}>
ログアウト成功!
</Paper>
<Link to='/'>ホーム</Link>
</div>
);
}
}
export default LogoutView;
6.パスワード変更
パスワード変更もログインすると表示されるようになります。APIとしてはAccounts.changePassword(cred.password1, cred.password2, ...)を使い、現在のパスワードと新パスワードを指定することでパスワードを変更します。
import React from 'react';
import ChangePasswordForm from '../components/ChangePasswordForm.js';
import { userIdFunc, handleErrorFunc } from '../App';
export default class LoginView extends React.Component {
constructor(props) {
super(props);
this.change = this.change.bind(this);
}
/* cred.password1=現パスワード、cred.password2=新パスワード、cred.password3=確認用パスワード */
change(cred) {
if(cred.password2 !== cred.password3) {
handleErrorFunc('エラー:確認用パスワードが一致しません。');
} else {
Accounts.changePassword(cred.password1, cred.password2, (err) => {
if (err) {
handleErrorFunc(err.message);
} else {
this.props.history.push({pathname: '/calendar'});
}
});
}
}
render () {
return (
<div>
<h1>パスワード変更画面</h1>
<div style={{maxWidth: 450, margin: '0 auto'}}>
<ChangePasswordForm
onSubmit={this.change} />
</div>
</div>
);
}
}