Expressでユーザー登録機能(メール認証つき、ダウンロード可)

こんにちは❗フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、以前このブログで公開した「コピペでOK!Expressにログイン機能をつくる」という記事では、ログイン機能をつくってみました。

そして、今回はその続きとして、

ユーザー登録する機能

をご紹介します。

誰でも登録できるオープンなサイトを公開するには、必須の機能ですよね。

そこで!

今回は以下の手順でユーザー登録できるようしてみます。

  1. メールアドレス&パスワードを送信
  2. 仮登録完了(本登録メールを送信)
  3. メールに書かれたURLをクリック
  4. 本登録が完了

ぜひみなさんのお役に立てると嬉しいです😊✨
(最後に今回開発したソースコード一式をダウンロードできます!)

開発環境: Node 8、Express 4.1

前提として

今回実装するユーザー登録機能には、以下4つのパッケージが必要になります。
それぞれインストールのコマンドと参考記事を用意しましたので、先に準備しておいてください。

また、テンプレートエンジンはmustacheを使っていますが、ejspugでも実行可能です。詳しくは「テンプレートエンジン」を使うをご覧ください。(ただし、他のテンプレートエンジンを使う場合はビューの拡張子が.mstではなくなりますのでご注意ください)

Sequelize

データベースを操作するパッケージ。

npm i --save sequelize
sudo npm i -g sequelize-cli

【参考記事】
Node.jsにDBマイグレーション、Seed、モデルを用意する「Sequelize」

express-validator

入力された内容をバリデーションするパッケージ。

npm i --save express-validator

【参考記事】
Expressでバリデーションする「express-validator」

nodemailer

メールを送信するパッケージです。

npm i --save nodemailer

【参考記事】
簡単!Expressでメール送信する方法

bcrypt

bcryptハッシュ値を計算するパッケージ(簡単に言うと、パスワードの暗号化です)

npm i --save bcrypt

【参考記事】
Expressにログイン機能をつくる

準備をする(Usersテーブル)

では、はじめにユーザー情報を管理するUsersテーブルを作っていきます。

以下のsequelize-cliコマンドを実行して、UserモデルとUsersテーブルのマイグレーションを作成してください。

npx sequelize-cli model:generate --name User --attributes name:string,email:string,emailVerifiedAt:date,password:string

すると、以下の2ファイルが作成されます。

  • /models/user.js
  • /migrations/**************-create-user.js

なお、emailVerifiedAtが今回のメール認証(本登録メール)に関連するフィールドです。

ここは初期状態ではNULLになっていますが、メール認証が完了したら、日時を登録することになります。(つまり本登録が完了しているかどうか判別できるようになります)

また、メールアドレスは重複させないようにしますので、マイグレーションにuniqueを追加しておいてください。

email: {
    type: Sequelize.STRING,
    unique: true
},

では、以下のコマンドでマイグレーションを実行しましょう。

npx sequelize-cli db:migrate

実際のDBテーブルは以下のようになります。

ユーザー登録フォームをつくる

では、ユーザー登録するフォームをつくっていきましょう。

app.js」に以下のルートを追加してください。

app.get('/register', (req, res) => {

  return res.render('auth/register');

});

そして、「views/auth/register.mst」というファイルを作成し、中身を以下のようにします。

<html>
<head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app">
    <div class="card-body">
        <div class="form-group row">
            <label for="name" class="col-md-4 col-form-label text-md-right">名前</label>
            <div class="col-md-6">
                <input type="text" class="form-control" v-model="params.name">
                <div class="alert alert-danger" v-if="errors.name" v-text="errors.name"></div>
            </div>
        </div>
        <div class="form-group row">
            <label for="email" class="col-md-4 col-form-label text-md-right">メールアドレス</label>
            <div class="col-md-6">
                <input type="email" class="form-control" v-model="params.email">
                <div class="alert alert-danger" v-if="errors.email" v-text="errors.email"></div>
            </div>
        </div>
        <div class="form-group row">
            <label for="password" class="col-md-4 col-form-label text-md-right">パスワード</label>
            <div class="col-md-6">
                <input type="password" class="form-control" v-model="params.password">
                <div class="alert alert-danger" v-if="errors.password" v-text="errors.password"></div>
            </div>
        </div>
        <div class="form-group row">
            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">パスワード(確認)</label>
            <div class="col-md-6">
                <input type="password" class="form-control" v-model="params.passwordConfirmation">
                <div class="alert alert-danger" v-if="errors.passwordConfirmation" v-text="errors.passwordConfirmation"></div>
            </div>
        </div>
        <div class="form-group row mb-0">
            <div class="col-md-6 offset-md-4">
                <button type="submit" class="btn btn-primary" @click="onSubmit">登録する</button>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                params: {
                    name: '',
                    email: '',
                    password: '',
                    passwordConfirmation: ''
                },
                errors: {}
            },
            methods: {
                onSubmit() {

                    this.errors = {
                        name: '',
                        email: '',
                        password: '',
                        passwordConfirmation: ''
                    };

                    axios.post('/register', this.params)
                        .then(response => {

                            if(response.data.result) {

                                alert('入力されたメールアドレスにメッセージ送信しました。');

                            }

                        })
                        .catch(error => {

                            const errors = error.response.data.errors;

                            errors.forEach(error => {

                                const key = error.param;

                                if(this.errors[key] === '') {

                                    this.errors[key] = error.msg;

                                }

                            });

                        });

                }
            }
        });

    </script>
</body>
</html>

中身としては、Vue + axiosでデータ送信するシンプルな構成になっています。

なお、ブラウザで表示するとこのようになります。

仮登録して本登録メールを送信する部分をつくる

では、今回のメインになりますが、少しやることが多いので分割して説明します。

パッケージとモデルの読み込み

先ほどインストールしたパッケージとSequlizeモデルが使えるようにします。(なお、インストール不要なcryptoパッケージも読み込んでいます)

const { check, validationResult } = require('express-validator');
const nodemailer = require('nodemailer');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const User = require('./models').User;

ミドルウェアの設定

ExpressではPOSTデータを取得する場合、以下のコードを追加しておく必要があります。

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

ユーザー登録に必要な事前データを用意する

できるだけシンプルにしようと心掛けているのですが、どうしてもコードが多くなってしまうので、せめていくつかを変数に格納しておくことにします。次の項目で追加するapp.post('/register', ...)ルートの直前に追加してください。

(さらにいうと、本来は.envなどを使うべきですが今回は割愛します。詳しくは、Node.jsで.envを使う方法をご覧ください)

// 暗号化につかうキー
const appKey = 'YOUR-SECRET-KEY';

// メール送信設定
const transporter = nodemailer.createTransport({
  host: '127.0.0.1',
  port: 1025,
  secure: '',
  auth: {
    user: '',
    pass: ''
  }
});

// バリデーション・ルール
const registrationValidationRules = [
  check('name')
    .not().isEmpty().withMessage('この項目は必須入力です。'),
  check('email')
    .not().isEmpty().withMessage('この項目は必須入力です。')
    .isEmail().withMessage('有効なメールアドレス形式で指定してください。'),
  check('password')
    .not().isEmpty().withMessage('この項目は必須入力です。')
    .isLength({ min:8, max:25 }).withMessage('8文字から25文字にしてください。')
    .custom((value, { req }) => {

      if(req.body.password !== req.body.passwordConfirmation) {

        throw new Error('パスワード(確認)と一致しません。');

      }

      return true;

    })
];

詳しい内容は、以下の記事をご覧ください。

送信されたデータをDBに保存し、本登録メールを送信する

では、送信されたデータをUserテーブルに登録し、さらに「本登録メール」を送信する部分です。

// ここに先ほどの事前データ

app.post('/register', registrationValidationRules, (req, res) => {

  const errors = validationResult(req);

  if(!errors.isEmpty()) { // バリデーション失敗

    return res.status(422).json({ errors: errors.array() });

  }

  // 送信されたデータ
  const name = req.body.name;
  const email = req.body.email;
  const password = req.body.password;

  // ユーザーデータを登録(仮登録)
  User.findOrCreate({
    where: { email: email },
    defaults: {
      name: name,
      email: email,
      password: bcrypt.hashSync(password, bcrypt.genSaltSync(8))
    }
  }).then(([user]) => {

    if(user.emailVerifiedAt) { // すでに登録されている時

      return res.status(422).json({
        errors: [
          {
            value: email,
            msg: 'すでに登録されています。',
            param: 'email',
            location: 'body'
          }
        ]
      });

    }
    // 本登録URLを作成
    const hash = crypto.createHash('sha1')
      .update(user.email)
      .digest('hex');
    const now = new Date();
    const expiration = now.setHours(now.getHours() + 1); // 1時間だけ有効
    let verificationUrl = req.get('origin') +'/verify/'+ user.id +'/'+ hash +'?expires='+ expiration;
    const signature = crypto.createHmac('sha256', appKey)
      .update(verificationUrl)
      .digest('hex');
    verificationUrl += '&signature='+ signature;

    // 本登録メールを送信
    transporter.sendMail({
      from: 'from@example.com',
      to: 'to@example.com',
      text: "以下のURLをクリックして本登録を完了させてください。\n\n"+ verificationUrl,
      subject: '本登録メール',
    });

    return res.json({
      result: true
    });

  });

});

なお、本登録メールで送信するURLはLaravelのコードを参考(というかほぼ移植)しました。いつもありがとうございます!

ちなみに、有効期限は1時間です。

本登録する部分をつくる

では最後に、本登録メールに記載されているURLにアクセスされたときのページをつくっていきましょう。

app.get('/verify/:id/:hash', (req, res) => {

  const userId = req.params.id;
  User.findByPk(userId)
    .then(user => {

      if(!user) {

        res.status(422).send('このURLは正しくありません。');

      } else if(user.emailVerifiedAt) {  // すでに本登録が完了している場合

        // ログイン&リダイレクト(Passport.js)
        req.login(user, () => res.redirect('/user'));

      } else {

        const now = new Date();
        const hash = crypto.createHash('sha1')
          .update(user.email)
          .digest('hex');
        const isCorrectHash = (hash === req.params.hash);
        const isExpired = (now.getTime() > parseInt(req.query.expires));
        const verificationUrl = APP_URL + req.originalUrl.split('&signature=')[0];
        const signature = crypto.createHmac('sha256', APP_KEY)
          .update(verificationUrl)
          .digest('hex');
        const isCorrectSignature = (signature === req.query.signature);

        if(!isCorrectHash || !isCorrectSignature || isExpired) {

          res.status(422).send('このURLはすでに有効期限切れか、正しくありません。');

        } else {  // 本登録

          user.emailVerifiedAt = new Date();
          user.save();

          // ログイン&リダイレクト(Passport.js)
          req.login(user, () => res.redirect('/user'));

        }

      }

    });

});

この中でやっていることは以下のとおりです。

  1. URLに含まれたユーザーIDからUserデータを取得
  2. メールアドレス、有効時間、正しいURLかをチェック
  3. 全てクリアしたら emailVerifiedAt に現在時間を保存

なお、ログイン&リダイレクトをする以下のコードが2ヶ所出てきますが、これはPassport.jsでのログインを想定しています。詳しくは、「Expressにログイン機能をつくる」をご覧ください。

req.login(user, () => res.redirect('/user'));

テストしてみる

では、実際にブラウザからテストしてみましょう。

まず、フォームに必要な入力をして送信します。

すると、以下のようなポップアップが表示されます。

メールを確認してみましょう。

URLをクリックする前に、Usersテーブルも確認してみます。

では、この状態でメールのURLをクリックし、再度Usersテーブルを確認してみましょう。

はい!
うまくemailVerifiedAtが更新されました😊✨

なお、わざと間違えたURLにアクセスすると以下のような表示になります。

お疲れ様でした!

ダウンロードする

以下から実際今回開発したソースコード一式をダウンロードすることができます。

※ただし、このコードを実行するには、passport.jsmustache-expressなどのパッケージも必要になりますので、ソースコードを確認してインストールしてください。

Expressでユーザー登録機能(メール認証つき)

おわりに

ということで、今回はExpressでユーザー登録機能をつくってみました。

実を言うと、この機能を開発しはじめた頃はもっと簡単にできるだろうと高をくくっていたのですが、実際は想像以上にやることが多く、なかなか大変な作業になってしまいました。

そんなこともあって、今回の記事の中で「パスワードリマインダー」も紹介しようと思っていましたが、それは別の記事したいと思います。

ただ、何度も同じようなことを言っていますが、今回の「ユーザー登録」と「ログイン」、そして「パスワードリマインダー」の3つがLaravelには標準で搭載されている(6.xからはパッケージに分離されましたが)ってやっぱりすごいですね。

Expressで開発をしようとすると開発費もその分跳ね上がるのは当然といっていいと思います。ちょっと違うかもしれませんが、IEでも動くサイトをつくろうとすると、それだけで開発費があがるというのと似ているかもしれません😅

今回はそんな感じです。

ではでは〜!

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ