シェルスクリプトマガジン

Node.js/Expressで楽々Webアプリ開発(Vol.59掲載)

著者:しょっさん

プログラミング言語「JavaScript」の実行環境「Node.js」と「Express」フレームワークを使って、基本となるWebアプリの開発手法を習得しましょう。最終回は、サンプルの「蔵書管理アプリケーション」の課題を解決し、「SPA」(Single Page Application)として実装します。

シェルスクリプトマガジン Vol.59は以下のリンク先でご購入できます。

図2 認証時にアクセストークンを発行する部分のプログラム

var express = require('express');
var router = express.Router();
const jwt = require('jsonwebtoken');
const models = require('../models'),
  User = models.user;

var app = express();

// パスワードハッシュ化
const hashPassword = (password, salt) => {
  if (password) {
    var bcrypt = require('bcrypt');
    return bcrypt.hashSync(password, salt);
  } else { return null; }
};

// ログイン処理
router.post('/', (req, res) => {
  const username = req.body.email;
  const password = req.body.password;
  User.findOne({ where: { email: username } })
    .then(user => {
      if (!user) {
        res
          .status(401)
          .json({ errors: { message: 'Incorrect email.' } });
      } else if (hashPassword(password, user.salt) !== user.password) {
        res
          .status(401)
          .json({ errors: { message: 'Incorrect password.' } });
      } else {
        const opts = {
          issuer: process.env.ISSUER,
          audience: process.env.AUDIENCE,
          expiresIn: process.env.EXPIRES,
        };
        const secret = process.env.SECRET;
        res
          .status(200)
          .json({ 'token': jwt.sign({ id: user.id }, secret, opts) });
      }
    });
});

図3 URLへのアクセスしたときのアクセストークンの検査部分のプログラム

const passport = require('passport');
const passportJWT = require('passport-jwt');
const ExtractJWT = passportJWT.ExtractJwt;
const JWTStrategy = passportJWT.Strategy;

// using authentication strategy
passport.use(new JWTStrategy(
  {
    jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
    issuer: process.env.ISSUER,
    audience: process.env.AUDIENCE,
    secretOrKey: process.env.SECRET
  }, (jwt_payload, done) => {
    User.findOne({ where: { id: jwt_payload.id } })
      .then(user => {
        if (user) {
          done(null, user);
        } else {
          done(null, false);
        }
      })
      .catch(err => {
        return done(err, false);
      });
  }
));

図4 カスタムコールバックを作成している部分のプログラム

// passport-jwt: custom callback
// Tokenを持っていなかったり、Invarid Tokenの場合、Json形式で返答しなかったりするので、カスタムコールバックで準備する必要がある
const jwt = (req, res, next) => {
  passport.authenticate('jwt', { session: false }, (err, user, info) => {
    if (err) { return next(err); }
    if (!user) {
      return res.status(401).json({ 'errors': { 'message': info || 'user unknown' } }).end();
    }
    req.user = user;
    next();
  })(req, res, next);
};

// ルーティング: 認証が必要なURIには、jwt関数コールを追記するだけでよい
app.use('/api/auth', auth);
app.use('/api/books', jwt, books);

図5 表現に合わせてJSON形式で返却する

  // 1冊の本の情報を取得する
  _get_book(book_id) {
    return libraries.findOne({
      where: {
        user_id: this._user_id,
        id: book_id
      },
      include: [{
        model: comments,
        required: false
      }]
    });
  }

  // 1冊の本の詳細を表示する
  find(req, res) {
    this._get_book(req.params.id)
      .then(result => {
        res
          .status(200)
          .json(result.get());
      }).catch(() => {
        res
          .status(200)
          .json({});
      });
  }

図6 エラー画面をJSON形式に変更する

// catch 404 and forward to error handler
app.use((req, res, next) => {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use((err, req, res, next) => {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.json({
    success: false,
    errors: {
      message: err.message
    }
  });
});

図7 テストコードを変更する

describe('with Login', () => {
  // store a jwt token
  let jwt_token;

  beforeAll((done) => {
    request(app)
      .post('/api/auth/')
      .send(userCredentials)
      .end((err, res) => {
        jwt_token = res.body.token;
        done();
      });
  });

  describe('GET /api/books/', () => {
    it('respond with REST', (done) => {
      request(app)
        .get('/api/books/')
        .set('Accept', 'application/json')
        .set('Authorization', Bearer ${jwt_token})
        .expect('Content-Type', /json/)
        .expect(200, done);
    });
  });
});