ExpressでJWT認証機能付きのAPIを作成する（その4. サインアップ時のメール認証・パスワードリセットの実装）

# はじめに
この記事は「その3. 認可機能とユーザ操作の実装」に続けて書いています。

## 目的
自社のFileMaker CloudデータベースにWebからアクセスするためのAPIを作成する必要があり，そのために勉強したことの備忘録として step by step で作成の過程を書いていきます。想定する読者はjavascriptを触ったことはあるけれども，本格的なバックエンド開発の経験は無い，というような方です。

この記事では不特定多数のユーザを対象とするサービスを構築する際に必須となるサインアップ時のメール認証やパスワードリセットの機能を，`Node.js`用のメーラーモジュールである`nodemailer`を用いて実装していきます。

## 開発環境
node v14.17.4
npm 6.14.14

ローカル開発環境には仮想SMTPサーバである[MailCatcher](https://mailcatcher.me/)がインストールされている前提です。

# Step 1. Nodemailerのセットアップ
```zsh:zsh
$ npm install nodemailer --save
```
SMTPサーバの設定は`.env`に記述しておき，それを読み込む。ついでに，後々使うであろうメール内容で必要になるサーバURLや署名なんぞも書き込んでおこう。
```.env
SERVER_URL=http://localhost:3000

# Nodemailer Settings
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USER=
MAIL_PASS=
MAIL_FROM=noreply@hoge.com
MAIL_FOOTER=ほげほげ株式会社（このメールには返信できません）
```
ユーザ管理関係のルーティングファイル`users.js`に`nodemailer`を読み込み，メール送信に必要な`transporter`オブジェクトを作成しておこう。
```javascript:/routes/users.js(一部)
const nodemailer = require('nodemailer')

const mailConfig = {
    host: process.env.MAIL_HOST,
    port: process.env.MAIL_PORT,
    auth: {
        user: process.env.MAIL_USER,
        pass: process.env.MAIL_PASS
    }
}
const transporter = nodemailer.createTransport(mailConfig)
```

## Step 2. ユーザテーブルの変更
メールによるサインアップ時の認証やパスワードリセットの機能を実装するため，既存のユーザテーブルに次のカラムを追加しよう。
* `emailVerifiedAt` *(date)*: メールによる認証が終了した日時を格納する。ここが空欄であれば，メールによる認証が済んでいないと見なしログイン認証を拒否する。
* `needPasswordReset` *(boolean)*: パスワードリセットが必要かどうかの論理値。ここが`1`であれば，パスワードリセットが済んでいないと見なしログイン認証を拒否する。

カラムの追加は`sequelize-cli`を用いて行う。
```zsh:zsh
$ npx sequelize-cli migration:create --name modify-user-for-using-mail
```
作成された空のマイグレーションファイルを編集する。
```diff_javascript:/migrations/yyyymmddhhssmm-modify-user-for-using-mail.js
'use strict';

  module.exports = {
    up: async (queryInterface, Sequelize) => {
+     await queryInterface.addColumn(
+       'Users',
+       'emailVerifiedAt',
+       {
+         type: Sequelize.DATE,
+         allowNull: true
+       }
+     )
+     await queryInterface.addColumn(
+       'Users',
+       'needPasswordReset',
+       {
+         type: Sequelize.BOOLEAN,
+         allowNull: true
+       }
+     )
    },

    down: async (queryInterface, Sequelize) => {
+     await queryInterface.removeColumn('Users', 'emailVerifiedAt'),
+     await queryInterface.removeColumn('Users', 'needPasswordReset')
    }
  };
```
カラムの追加は`queryInterface.addColumn()`で行える。引数にはMySQLのテーブル名，カラム名とオプションを指定する。`down:`（マイグレーションを取り消す際の処理）に`queryInterface.removeColumn()`を記述することを忘れずに。

さらに，モデルファイルの方も書き換えておこう。
```diff_javascript:/models/user.js（一部）
  User.init({
    id: {
      primaryKey: true,
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4
    },
    username: DataTypes.STRING,
    email: DataTypes.STRING,
    password: DataTypes.STRING,
    privilege: DataTypes.INTEGER,
+   emailVerifiedAt: DataTypes.DATE,
+   needPasswordReset: DataTypes.BOOLEAN
  }, {
    sequelize,
    modelName: 'User',
  })
```
編集が終わったらマイグレーションを実行してデータベースに反映させる。
```zsh:zsh
$ npx sequelize-cli db:migrate
```
これで準備完了♪

# Step 3. 機能実装
それではエンドポイントを実装していこう。

## 3-1. サインアップ（修正）
既に作成してあるサインアップのコードを修正し，ユーザに認証メールを送信するようにする。アドミニストレータが直接メールアドレスを指定してサインアップする時などは，リクエストヘッダに有効なAccess Tokenを送信し，`skipVerification`を`true`にすればメール送信を回避するよう設定する。

### 仕様
**Method/end-point**
　　**POST** /users
**Request Headers**
　　**Content-type** : application/JSON
　　**X-Acces-Token** : Access Token（メール認証なしでサインアップを行う時のみ，有効なアドミニストレータ権限の記載されたAccess Tokenが必要）
**Request Body**
　　**username** *(string)* : ユーザ名（必須）
　　**email** *(string)* : メールアドレス（必須）
　　**password** *(string)* : パスワード（必須）
　　**privilege** *(integer)* : 権限（0:一般ユーザ，1：管理ユーザ，2：アドミニストレータ。1, 2以外を指定すると0となる。）
　　**skipVerification** *(boolean)* : `true`に設定された場合，メールによる認証処理を行わない。アドミニストレータ権限の記載されたAccess Tokenがヘッダに無い場合には無視される。
**Status Code**
　　**200 OK** : 正常に登録（認証メールの送信を含む）された
　　**400 Bad Request** : リクエスト不正
　　**409 Conflict** : ユーザ名が既に使用されている
　　**500 Internal Server Error** : サーバ処理異常
**Response JSON object**
　　**id** *(string)* : 主キー
　　**email** *(string)* : メールアドレス
　　**privilege**　*(integer)* : 権限
　　**username** *(string)* : ユーザ名
　　**emailVerifiedAt** *(string)* : メール認証済みの場合，メール認証日時が返る
　　**needPasswordReset** *(boolean)* : パスワードリセットを要する場合，`true`が返る
　　**updatedAt** *(string)* : ユーザ更新日時
　　**createdAt** *(string)* : ユーザ作成日時
　　**verificationLimit** *(string)* : 認証メールが送信された場合，認証期限が返る

### コード
まず`User`モデルのインスタンスを引数として認証メールを送信する関数をPromiseベースで作成しておく。
```javascript:/routes/users.js
const sendVerificationMail = (user) => {
  return new Promise((resolve, reject) => {
    // メール認証用のJWTを作成
    const jwtPayload = {
      id: userInst.id
    }
    const mailToken = jwt.sign(jwtPayload, jwtSecret, jwtOptions)
    const exp = JSON.parse(Buffer.from(mailToken.split('.')[1], 'base64').toString()).exp
    const mailTokenExpiryDate = new Date(exp * 1000)
    // メール内容を作成して送信
    const verificationURL = process.env.SERVER_URL + '/users/verify/' + mailToken
    const message = {
      from: process.env.MAIL_FROM,
      to: userInst.email,
      subject: 'ほげほげシステム　本登録メール',
      html: '<p>' + user.username + 'さんの本登録を行います。</p>'
          + '<p>以下のURLから本登録を完了してください。</p>'
          + '<p><a href=' + verificationURL + '>' + verificationURL + '</a></p>'
          + '<p>URL有効期限：' + mailTokenExpiryDate.toLocaleString('ja-JP', {timeZone: 'JST'}) + '（日本時間）</p>'
          + '<hr>'
          + '<p>' + process.env.MAIL_FOOTER + '</p>'
    }
    try {
      transporter.sendMail(message)
      resolve(mailTokenExpiryDate)
    } catch (err) {
      reject(err)
    }
  })
}
```
まず認証用のトークンをJWT形式で生成する。Access Tokenとは違い，ペイロードはユーザの`id`のみとしている。生成されたトークンから有効期限を取り出すところは一緒。
```javascript
const jwtPayload = {
  id: userInst.id
}
const mailToken = jwt.sign(jwtPayload, jwtSecret, jwtOptions)
const exp = JSON.parse(Buffer.from(mailToken.split('.')[1], 'base64').toString()).exp
const mailTokenExpiryDate = new Date(exp * 1000)
```
トークンが生成されれば次にメール内容を作成し，送信する。
```javascript
const verificationURL = process.env.SERVER_URL + '/users/verify/' + mailToken
const message = {
  from: process.env.MAIL_FROM,
  to: userInst.email,
  subject: 'ほげほげシステム　本登録メール',
  html: '<p>' + user.username + 'さんの本登録を行います。</p>'
      + '<p>以下のURLから本登録を完了してください。</p>'
      + '<p><a href=' + verificationURL + '>' + verificationURL + '</a></p>'
      + '<p>URL有効期限：' + mailTokenExpiryDate.toLocaleString('ja-JP', {timeZone: 'JST'}) + '（日本時間）</p>'
      + '<hr>'
      + '<p>' + process.env.MAIL_FOOTER + '</p>'
}
try {
  transporter.sendMail(message)
  resolve(mailTokenExpiryDate)
} catch (err) {
  reject(err)
}
```
認証用URLは`.env`から取得したサーバURLに続けて`/users/verify/トークン`という形式にしている。認証用URLを生成したら，送信するメール内容を記述した`message`オブジェクトを作成する。最後に`transporter.sendMail(message)`で送信。送信処理は`try {}`節に入れてあるので，送信が成功すれば`resolve(mailTokenExpiryDate)`で認証期限とともに`resolve`を返し，SMTPサーバのダウン等でエラーが返ってくれば`reject(err)`でエラーとともに`reject`を返す。

それではメインの処理を修正していこう。
```diff_javascript:/routes/users.js
  router.post('/', (req, res, next) => {
+   // トークンがあれば検証する
+   if (req.headers['x-access-token']) {
+     const token = req.headers['x-access-token']
+     jwt.verify(token, jwtSecret, (err, decoded) => {
+       if (err) return new CustomError('Unauthorized: ' + err.message + '.', 400)
+       req.privilege = decoded.privilege
+     })
+   }
    if (!Object.keys(req.body).length) return next(new CustomError('Request is invalid.', 400))
    if (!req.body.username || !req.body.email || !req.body.password) return next(new CustomError('Username, email and password are mandatory.', 400))
    if (req.body.privilege !== 1 && req.body.privilege !== 2) req.body.privilege = 0
    const newPasswordHash = bcrypt.hashSync(req.body.password, salt)
    models.User.findOrCreate({
      where: { username: req.body.username },
      defaults: { email: req.body.email, password: newPasswordHash, privilege: req.body.privilege }
-   }).then(([userInst, created]) => {
+   }).then(async ([userInst, created]) => {
      if (!created) return next(new CustomError('The username already exists.', 409))
      let resJSON = { ...userInst.dataValues }
+     // アドミニストレータユーザがメール認証スキップを指定した場合には，emailVerifiedAtに現在時刻を設定する
+     if (req.privilege === 2 && req.body.skipVerification === true) {
+       await userInst.update({ emailVerifiedAt: new Date })
+     } else {
+       const verificationLimit = await sendVerificationMail(userInst)
+       resJSON.verificationLimit = verificationLimit
+     }
      delete resJSON.password
      res.json(resJSON)
    }).catch((err) => {
      return next(new CustomError(err.message, 500))
    })
  })
```
まずアドミニストレータがメール認証を回避してユーザ追加を行いたい場合にはAccess Tokenを送ってくるので，Access Tokenの検証を最初に行っている。ここは認証ミドルウェアとやっていることは同じ。

ユーザを`findOrCreate`で検索／作成して，ユーザインスタンスが返ってきてからの`.then()`節は元々同期処理だったが，この後で`await`を使うので`async`を追加している。
```javascript
if (req.privilege === 2 && req.body.skipVerification === true) {
  await userInst.update({ emailVerifiedAt: new Date })
} else {
  const verificationLimit = await sendVerificationMail(userInst)
  resJSON.verificationLimit = verificationLimit
}
```
ユーザがアドミニストレータであればAccess Tokenを検証した際に`req.privilege`に`2`が設定されているはず。そのうえでリクエストボディ`skipVerification`が`true`に設定されていれば，ユーザインスタンスの`emailVerifiedAt`に現在時刻を設定して`update`でデータを保存する。

もしそうでなければ`sendVerificationMail(userInst)`を呼び出して認証メールを送信する。問題なく認証メールが送信されれば認証期限が返ってくるはずなので，これを応答JSONに追加している。

実装が終わったら試しにサインアップしてみる。MailCatcherで確認してみると
![スクリーンショット 2022-04-07 18.36.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1734470/e13c4496-0b2d-f30f-b939-c4ec59c0b648.png)
しっかりメールが送信されていることがわかる。

## 3-2. 認証メールの再送
送信された認証URLに含まれるトークンはAccess Tokenと同じ有効時間を持っている（上記の`sendVerificationMail`関数の中で使用している`jwtOptions`を変更すれば，別の有効時間を持たせることは可能）。したがって，期限が過ぎてしまうともうその認証URLは使用できなくなってしまう。

実運用では「メールが送られたのに開いてクリックするのを忘れた」というユーザが出現することは当然予想されることなので，認証メールの再送機能は絶対に必要。`username`を送信することで認証メールを再送信するエンドポイントを作成しておこう。

### 仕様
**Method/end-point**
　　**POST** /users/verify
**Request Headers**
　　**Content-type** : application/JSON
**Request Body**
　　**username** *(string)* : ユーザ名（必須）
**Status Code**
　　**200 OK** : 正常に認証メールが送信された
　　**400 Bad Request** : リクエスト不正
　　**404 Not Found** : ユーザが存在しない
　　**409 Conflict** : 既にメール認証が行われている
　　**500 Internal Server Error** : サーバ処理異常
**Response JSON object**
　　**id** *(string)* : 主キー
　　**email** *(string)* : メールアドレス
　　**privilege**　*(integer)* : 権限
　　**username** *(string)* : ユーザ名
　　**updatedAt** *(string)* : ユーザ更新日時
　　**createdAt** *(string)* : ユーザ作成日時
　　**verificationLimit** *(string)* : 認証メールの認証期限が返る

### コード
```javascript:/routes/users.js
router.post('/verify', (req, res, next) => {
    // リクエスト検証
    if (!Object.keys(req.body).length) return next(new CustomError('Request is invalid.', 400))
    if (!req.body.username) return next(new CustomError('Username is required.', 400))
    // ユーザ検索
    models.User.findOne({ where: { username: req.body.username } })
        .then(async (userInst) => {
            if (!userInst) return next(new CustomError('User not found.', 404))
            if (userInst.emailVerifiedAt) return next(new CustomError('Email already verified.', 409))
            let resJSON = { ...userInst.dataValues }
            const verificationLimit = await sendVerificationMail(userInst)
            resJSON.verificationLimit = verificationLimit
            res.json(resJSON)
        }).catch((err) => {
            return next(new CustomError(err.message, 500))
        })
})
```
リクエストに`username`が含まれるか検証し，含まれていればそれを元にユーザを検索。返ってきたユーザインスタンスから重複をチェックし，認証メールを送信するところはサインアップとほぼ同じだ。

## 3-3. 認証用URL
認証用URLを受け取るエンドポイント。トークンがURLに含まれるので，`req.params`で取得して処理を行っていく。通常のAPIエンドポイントと違い，ユーザのメールソフトから直接クリックされるので，応答はJSONではなく，一般ユーザが見て内容を理解できる形で返す必要がある。

### 仕様
**Method/end-point**
　　**GET** /users/verify/*(token)*
**Status Code**
　　**200 OK** : 正常に認証メールが送信された
　　**400 Bad Request** : リクエスト不正
　　**404 Not Found** : ユーザが存在しない
　　**409 Conflict** : 既にメール認証が行われている
　　**500 Internal Server Error** : サーバ処理異常
**Response JSON object**
　　**id** *(string)* : 主キー
　　**email** *(string)* : メールアドレス
　　**privilege**　*(integer)* : 権限
　　**username** *(string)* : ユーザ名
　　**updatedAt** *(string)* : ユーザ更新日時
　　**createdAt** *(string)* : ユーザ作成日時
　　**verificationLimit** *(string)* : 認証メールの認証期限が返る

### コード
まず，認証が正常に終了した際にユーザに遷移してもらうフロントエンドのトップURLを`.env`に記述しておく。
```.env
TOP_URL=http://localhost:8080
```
その上で，エンドポイントのコードは以下のようにした。
```javascript:/routes/users.js
router.get('/verify/:token', (req, res, next) => {
    const token = req.params.token
    jwt.verify(token, jwtSecret, (err, decoded) => {
        if (err) {
            if (err.message = 'jwt expired') return res.status(401).send('認証URLが失効しています。ログイン画面から「認証URLの再送」へ進んで下さい。<br><a href=' + process.env.TOP_URL + '>ログイン画面へ</a>')
            return res.status(401).send('認証URLが不正です。')
        }
        models.User.findByPk(decoded.id)
            .then(async (userInst) => {
                if (!userInst) return res.status(404).send('ユーザが見つかりません。')
                if (userInst.emailVerifiedAt) return res.status(409).send('ユーザは既に認証されています。<br><a href=' + process.env.TOP_URL + '>ログイン画面へ</a>')
                await userInst.update({ emailVerifiedAt: new Date })
                res.send(userInst.username + 'さんのメール認証が正常に行われました。<br><a href=' + process.env.TOP_URL + '>ログイン画面へ</a>')
            })
    })
})
```
URLから取り出したトークンはJWT形式なので，認証処理と同じく`jwt.verify`で検証を行っている。エラーのうち，認証期限切れの場合には`err.message = 'jwt expired'`で検出できるため，特別に期限切れであることを示すメッセージを返している。

トークンの検証が通ればペイロードに含まれる`id`を元にユーザを検索する。返ってきたインスタンスで`emailVerifiedAt`に値があれば既に認証されているということなので`409`エラーを返している。値がなければ現在時刻を入力してアップデートし，正常に処理が終了した旨のメッセージを返して終了。

実際に送信されたメールのURLをクリックしてみると
![スクリーンショット 2022-04-07 19.57.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1734470/b258e806-0000-1e69-43ee-afb5473cf6c1.png)
正常に処理が行われている。

## 3-4. パスワードリセット
ユーザがパスワードを忘れた時のために，ユーザネームを元に登録されたメールアドレスへ新しいパスワードを送信するエンドポイントを作成する。

### 仕様
**Method/end-point**
　　**POST** /users/password-reset
**Request Headers**
　　**Content-type** : application/JSON
**Request Body**
　　**username** *(string)* : ユーザ名（必須）
**Status Code**
　　**200 OK** : 正常にパスワードリセットメールが送信された
　　**400 Bad Request** : リクエスト不正
　　**401 Unauthorized** : メール認証がされていない
　　**404 Not Found** : ユーザが存在しない
　　**500 Internal Server Error** : サーバ処理異常
**Response JSON object**
　　**id** *(string)* : 主キー
　　**email** *(string)* : メールアドレス
　　**privilege**　*(integer)* : 権限
　　**username** *(string)* : ユーザ名
　　**updatedAt** *(string)* : ユーザ更新日時
　　**createdAt** *(string)* : ユーザ作成日時

### 下準備
パスワードリセットの際は，新たにランダムな文字列のパスワードを作成し，メールでユーザに送付するとともにそのハッシュをデータベースに書き込む。ランダムな文字列の生成はネットでググれば色々出てくるけど，今回は[ここ](https://qiita.com/fukasawah/items/db7f0405564bdc37820e)を参考にして，再利用しやすいようにランダム文字列生成用のクラスを作成することにした。
```javascript:/routes/users.js
class RandomPass {
    constructor() {
        this.chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
    }
    generate(length) {
        return Array.from(Array(length)).map(() => this.chars[Math.floor(Math.random() * this.chars.length)]).join('')
    }
}
const randomPass = new RandomPass
```
`constructor()`でこのクラスを元にインスタンスが生成される際，`chars`にパスワードに使用する文字列を格納している。なお，間違えやすい`l` `I` `1`と`o` `O` `0`は抜いている。

`generate`メソッドはパスワードの文字数`length`を引数にとり，そのサイズの配列を生成した上で，各文字位置に`.map`で`chars`からランダムな文字を充当させている。

クラス定義が終われば，`randomPass`をインスタンスとして生成し，以下の処理で利用する。

### コード
```javascript:/routes/users.js
router.post('/password-reset', (req, res, next) => {
  // リクエスト検証
  if (!Object.keys(req.body).length) return next(new CustomError('Request is invalid.', 400))
  if (!req.body.username) return next(new CustomError('Username is required.', 400))
  // ユーザ検索
  models.User.findOne({ where: { username: req.body.username } })
    .then(async (userInst) => {
      // エラー処理
      if (!userInst) return next(new CustomError('User not found.', 404))
      if (!userInst.emailVerifiedAt) return next(new CustomError('Mail address is not verified.', 401))
      // 新規をパスワード発行してデータベースへ反映
      const newPassword = randomPass.generate(8)
      const newPasswordHash = bcrypt.hashSync(newPassword, salt)
      await userInst.update({
        password: newPasswordHash,
        needPasswordReset: true
      })
      // メール送信
      const message = {
        from: process.env.MAIL_FROM,
        to: userInst.email,
        subject: 'ほげほげシステム　パスワード再設定メール',
        html: '<p>' + userInst.username + 'さんのパスワードをリセットしました。</p>'
          + '<p>パスワード：' + newPassword + '</p>'
          + '<p>ログイン後，パスワードを変更して下さい。<a href=' + process.env.TOP_URL + '>ログイン画面へ</a></p>'
          + '<hr>'
          + '<p>' + process.env.MAIL_FOOTER + '</p>'
      }
      transporter.sendMail(message)
      let resJSON = { ...userInst.dataValues }
      res.json(resJSON)
    }).catch((err) => {
      return next(new CustomError(err.message, 500))
    })
})
```
リクエストを検証した後，ユーザ名を元にユーザを検索するところまでは認証メール再送信と一緒。エラー処理では，ユーザが見つからなかった時に加え，ユーザのメールアドレスがまだ検証されていない場合にもエラーを返している。（ユーザ本人のものか検証されていないアドレスにパスワードを送付するわけにはいかないもんね）

問題なければ新規パスワードを`randomPass.generate()`で生成し，ハッシュをデータベースへ登録，メール送信。

## 3-5. ログイン（修正）
ログインのエンドポイントは，ユーザインスタンスで`emailVerifiedAt`に値が入っているかどうかを見れば良い。入っていなければまだメール認証がされていないということなので，`401`エラーを返す。

### コード
```diff_javascript:/routes/users.js
  router.post('/login', (req, res, next) => {
    if (!Object.keys(req.body).length) return next(new CustomError('Request is invalid.', 400))
    if (!req.body.username || !req.body.password) return next(new CustomError('Username and password are mandatory.', 400))
    models.User.findOne({
      where: { username: req.body.username }
    }).then(async (userInst) => {
      if (!userInst) return next(new CustomError('User not found.', 404))
      const passwordValid = bcrypt.compareSync(req.body.password, userInst.password)
      if (!passwordValid) return next(new CustomError('Password is invalid.', 401))
+     if (!userInst.emailVerifiedAt) return next(new CustomError('Mail address is not verified', 401))
      const tokens = await Promise.all([
        generateAccessToken(userInst),
        generateRefreshToken(userInst, true, true) // generate new token and set duration.
      ])
      let resJSON = { ...userInst.dataValues, ...tokens[0], ...tokens[1] }
      delete resJSON.password
      res.json(resJSON)
    }).catch((err) => {
      return next(new CustomError(err.message, 500))
    })
  })
```

## 3-6. ユーザ情報編集（修正）
パスワードリセット後のユーザは`needPasswordReset`が`true`となるので，これをもとにフロントエンド側でパスワードの変更を促す。パスワードが変更されれば`needPasswordReset`フラグを解除しなければならないので，その機能を実装する。

### コード
長いので，ユーザ検索後の`.then()`節のみ。
```diff_javascript:/routes/users.js
  models.User.findByPk(req.params.id)
    .then(async (userInst) => {
      if (!userInst) return next(new CustomError('User not found.', 404))
      if (req.body.username && (req.body.username !== userInst.username)) {
        const userCount = await models.User.count({ where: { username: req.body.username } })
        if (userCount) return next(new CustomError('This username is already used.', 409))
      }
      if (req.body.username) userInst.username = req.body.username
      if (req.body.email) userInst.email = req.body.email
-     if (req.body.password) userInst.password = bcrypt.hashSync(req.body.password, salt)
+     if (req.body.password) {
+       userInst.password = bcrypt.hashSync(req.body.password, salt)
+       if (userInst.needPasswordReset) userInst.needPasswordReset = null
+     }
      if (!(req.body.privilege === undefined)) userInst.privilege = req.body.privilege
      // Save data and return response
      const updatedUser = await userInst.save()
      let resJSON = { ...updatedUser.dataValues }
      delete resJSON.password
      res.json(resJSON)
    })
```
パスワードの変更があった場合，`needPasswordReset`を`null`に戻している。

# まとめ
ここまでで，ユーザ管理関係の基本的なCRUDに加え，メールによる認証やパスワードリセットのバックエンド機能を実装してきました。ここらで記事をあらため，いよいよフロントエンドの開発に移りたいと思います。