Takumon/mean-blog

View on GitHub
server/routes/authenticate.ts

Summary

Maintainability
D
2 days
Test Coverage
import * as http from 'http';
import { Router, Response } from 'express';
import * as jwt from 'jsonwebtoken';
import * as jdenticon from 'jdenticon';
import { check, oneOf, body, validationResult } from 'express-validator/check';
import { matchedData, sanitize } from 'express-validator/filter';

import { User, UserDocument } from '../models/user.model';
import { Image, ImageType } from '../models/image.model';
import * as ENV from '../environment-config';
import { PasswordManager } from '../helpers/password-manager';
import { validateHelper as v } from '../helpers/validate-helper';
import { sysError, validationError, cudSuccess, forbiddenError } from '../helpers/response-util';

const router: Router = Router();

router.post('/login', [
  body('userId')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['ユーザID'])),
  body('password')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['パスワード'])),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

  const reqUser = req.body;
  User.findOne({
    userId: reqUser.userId,
    deleted: { $eq: null}
  }, function(err, user) {
    if (err) {
      return sysError(res, {
        title: v.MESSAGE_KEY.default,
        error: err.message
      });
    }

    if (!user || !PasswordManager.compare(reqUser.password, user.password)) {
      return validationError(res, [{
        param: 'common',
        msg: v.message(v.MESSAGE_KEY.login_error),
        value: '',
        location: 'body'
      }]);
    }

    const token = jwt.sign({ _id: user._id }, ENV.SECRET, {
      expiresIn: ENV.TOKEN_EFFECTIVE_SECOND
    });

    // パスワードはクライアント側に送信しない
    deleteProp(user, 'password');

    res.json({
      success: true,
      message: '認証成功',
      token: token,
      user: user,
    });
  });
});

function isAllreadyUsed(userId: String): Promise<boolean> {
  return User
  .findOne({ userId: userId, deleted: { $eq: null}})
  .exec()
  .then(user => {
    if (user) {
      return Promise.reject(true);
    }
    // チェックOK
    return Promise.resolve(true);
  }).catch(err => Promise.reject(false));
}

router.post('/register', [
  body('userId')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['ユーザID']))
    .isLength({ min: 6 }).withMessage(v.message(v.MESSAGE_KEY.minlength, ['ユーザID', '6']))
    .isLength({ max: 30 }).withMessage(v.message(v.MESSAGE_KEY.maxlength, ['ユーザID', '30']))
    .matches(v.PATTERN.HANKAKUEISU).withMessage(v.message(v.MESSAGE_KEY.pattern_hankakueisuji, ['ユーザID']))
    .custom(isAllreadyUsed).withMessage(v.message(v.MESSAGE_KEY.allready_existed, ['ユーザID'])),
  body('password')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['パスワード']))
    .matches(v.PATTERN.PASSWORD).withMessage(v.message(v.MESSAGE_KEY.pattern_password, ['パスワード'])),
  body('confirmPassword')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['確認用パスワード']))
    .custom((value, {req}) => value === req.body.password).withMessage(v.message(v.MESSAGE_KEY.different, ['パスワード', '確認用パスワード'])),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return validationError(res, errors.array());
  }

  const reqUser = req.body;
  User.findOne({
    userId: reqUser.userId,
    deleted: { $eq: null}
  }, (err, user) => {
    if (err) {
      return sysError(res, {
        title: v.MESSAGE_KEY.default,
        error: err.message
      });
    }

    const newUser = new User();
    newUser.userId = reqUser.userId;
    newUser.password = PasswordManager.crypt(reqUser.password);


    newUser.save( (err2) => {
      if (err2) {
        return sysError(res, {
          title: v.MESSAGE_KEY.default,
          error: err2.message
        });
      }

      const token = jwt.sign({ _id: newUser._id }, ENV.SECRET, {
        expiresIn : ENV.TOKEN_EFFECTIVE_SECOND
      });


      // アバター登録
      const avator = new Image({
        author: newUser._id,
        data: jdenticon.toPng(reqUser.userId, 200),
        contentType: 'image/png',
        fileName: `avator_${newUser.userId}.png`,
        type: ImageType.AVATOR,
      });


      avator.save((err3) => {
        if (err3) {
          return sysError(res, {
            title: v.MESSAGE_KEY.default,
            error: err3.message
          });
        }

        // パスワードはクライアント側に送信しない
        deleteProp(newUser, 'password');

        return res.send({
          success: true,
          message: 'ユーザ情報を新規作成しました。',
          token: token,
          user: newUser,
        });
      });

    });
  });
});


function isCollectPassword(password, {req}) {
  return User
  .findOne({ _id: req.body._id, deleted: { $eq: null}})
  .exec()
  .then(user => {
    if (user && PasswordManager.compare(password, user.password)) {
        return Promise.resolve(true);
    }
    return Promise.reject(false);
  }).catch(err => Promise.reject(false));
}


router.put('/changepassword', [
  body('_id')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['ユーザID']))
    .custom(v.validation.isExistedUser).withMessage(v.message(v.MESSAGE_KEY.not_existed, ['ユーザID'])),
  body('oldPassword')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['現在のパスワード']))
    .custom(isCollectPassword).withMessage(v.message(v.MESSAGE_KEY.invalid, ['現在のパスワード'])),
  body('newPassword')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['新しいパスワード']))
    .matches(v.PATTERN.PASSWORD).withMessage(v.message(v.MESSAGE_KEY.pattern_password, ['新しいパスワード']))
    .custom((value, {req}) => value !== req.body.oldPassword).withMessage(v.message(v.MESSAGE_KEY.same, ['現在のパスワード', '新しいパスワード'])),
  body('newConfirmPassword')
    .not().isEmpty().withMessage(v.message(v.MESSAGE_KEY.required, ['新しいパスワード確認用']))
    .custom((value, {req}) => value === req.body.newPassword).withMessage(v.message(v.MESSAGE_KEY.different, ['新しいパスワード', '新しいパスワード確認用'])),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return validationError(res, errors.array());
  }

  const model = {$set: {
    password: PasswordManager.crypt(req.body.newPassword),
    updated: new Date()
  }};

  User.findByIdAndUpdate(req.body._id, model, {new: true}, (err, registeredUser: UserDocument) => {
    if (err) {
      return sysError(res, {
        title: v.MESSAGE_KEY.default,
        error: err.message
      });
    }

    // パスワードはクライアント側に送信しない
    deleteProp(registeredUser, 'password');

    return cudSuccess(res, {
      message: `パスワードを更新しました。`,
      obj: registeredUser
    });
  });
});


/**
 * 認証済かチェックする
 */
router.get('/check-state', (req, res) => {
  const token = req.body.token || req.query.token || req.headers['x-access-token'];

  if (!token) {
    res.send({
      success: false,
      message: 'トークンが存在しません。',
    });

    return;
  }

  jwt.verify(token, ENV.SECRET, (err, decoded) => {
    if (err) {
      return res.send({
        success: false,
        message: 'トークン認証に失敗しました。',
        error: err.message
      });
    }

    if (!decoded) {
      return res.send({
        success: false,
        message: 'トークン認証に失敗しました。',
      });
    }

    User
      .find({
        _id: decoded._id,
        deleted: { $eq: null}
      })
      .select('-password')
      .exec( (err2, doc) => {
        if (err2) {
          return res.send({
            success: false,
            message: 'ログインユーザ情報が取得できませんでした。',
            error: err.message
          });
        }

        if (!doc[0]) {
          return res.send({
            success: false,
            message: 'ログインユーザ情報が取得できませんでした。',
          });
        }

        return res.send({
          success: true,
          message: '認証済',
          token: token,
          user: doc[0],
        });
      });
  });
});

/**
 * delete演算子の代役
 * deleteだとプロパィが削除できないので
 * 回避策としてundefinedを代入する(値がundefinedの場合プロパティ自体レスポンスに定義されない)
 *
 * @param obj
 * @param propertyName
 */
function deleteProp(obj: Object, propertyName: string) {
  if (propertyName in obj) {
    obj[propertyName] = undefined;
  }
}

export { router as authenticateRouter};