2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

meteorのaccounts-passwordを使ったログイン認証画面のカスタマイズ

Last updated at Posted at 2017-09-10

 MeteorはWebアプリを超高速に作れるプラットフォームで、いろいろ便利な機能を提供してくれているけど、その中でもユーザアカウント機能は本当に超簡単に追加することができます。accounts-password と accounts-uiの2つのパッケージをaddするだけで、ユーザ登録とログイン認証機能がすぐに実装できます。UI込みでです。(但しreactを使う時は少しのコーディングが必要ですが。)

 今回はaccounts-uiを使わずに、React + React-router + Material-ui でカスタマイズ画面を作って、以下の機能を実装しましたのでその報告です。accounts-passwordの機能だけを使っています。accounts-uiを使う場合はあまりにも簡単なのですが、使わない場合の例があまり見当たらないので、備忘録も兼ての報告です。

  1. ユーザ登録(メール確認無し)
  2. ユーザ登録(メール確認あり)
  3. パスワード再発行
  4. ログイン
  5. ログアウト
  6. パスワード変更

 実際にはマルチユーザ対応のカレンダーアプリを作りながら試しましたが、まずは以下のパッケージをインストールします。(カレンダーアプリについては公開できる時が来たら、またその報告を書きたいと思います。)

$ 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のイベントハンドラーでユーザ登録を行います。

imports/ui/views/RegisterView.js

 ここのポイントはAccounts.createUser()を使って、クライアントから直接ユーザ登録を行っている点です。サーバ側のコードは必要ありません。これで登録したメールアドレスとパスワードでログインできるようになります。下位のRegisterFormのコードも示しておきます。

imports/ui/components/RegisterForm.js
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.ユーザ登録(メール確認あり)

 ユーザ登録時にメール確認を必要とする場合です。今回は、メール送信を行いますので、サーバ側のコードを必要とします。ユーザ登録画面にはパスワード入力欄の必要はありません。以下にコードを示します

imports/ui/views/RegisterView.js
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欄が無い点のみが異なります。

imports/ui/components/RegisterForm.js
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のサーバ側コードを書いておきます。

server/main.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.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です。

url置換の例
【置換前】http://www.mypress.jp:3000/#/enroll-account/T9EH-_B52wKgci-_LrljWjEk1yJdj0LF5D14q88pw3o
【置換後】http://www.mypress.jp:3000/reset2-password/T9EH-_B52wKgci-_LrljWjEk1yJdj0LF5D14q88pw3o

 次にサーバ側のmethodです。

imports/api/events.js
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をクリックするとパスワードリセット画面に飛びます。

imports/ui/views/ResetPasswordView.js
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定義
 <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は他と同じですので省略します。

imports/ui/RecoverView.js
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のカスタマイズに従って、入力されたメールアドレス宛にメールを送信します。

server/main.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は今までと同じようですので省略します。

imports/ui/views/LoginView.js
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()を呼んですぐにログアウトするようにしています。

imports/ui/views/LogoutView.js
"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, ...)を使い、現在のパスワードと新パスワードを指定することでパスワードを変更します。

imports/ui/views/ChangePasswordView.js
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>
    );
  }
}
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?