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

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

著者:しょっさん
プログラミング言語「JavaScript」の実行環境「Node.js」と「Express」フレームワークを使って、基本となるWebアプリの開発手法を習得しましょう。第4回は「蔵書管理アプリケーション」のサンプルプログラムで認証機能を実現す
る方法を解説します。

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

図3 「models/user.js」ファイルにLibrariesテーブルとの関係性を追記
user.associate = function (models) {
  // associations can be defined here
  user.hasMany(models.Library, { foreignKey: 'user_id' });
};
return user;return user;
図4 モデルファイル「models/library.js」の修正
'use strict';
module.exports = (sequelize, DataTypes) => {
  var Library = sequelize.define('Library', {
    book_title: DataTypes.STRING,
   author: DataTypes.STRING,
    publisher: DataTypes.STRING,
    image_url: DataTypes.STRING(2048),
    user_id: DataTypes.INTEGER
  },
    {
      underscored: true
    });
  Library.associate = function (models) {
    // associations can be defined here
    Library.hasMany(models.Comment, { foreignKey: 'book_id'});
  };
  return Library;
};
図5 マイグレーションファイルの追記内容
user_id: {
  type: Sequelize.INTEGER,
  allowNull: false,
  foreignKey: true,
  references: {
    model: 'users',
    key: 'id',
  },
  onUpdate: 'RESTRICT',
  onDelete: 'RESTRICT',
},
図6 作成したseed(日付-demo-user.js)
'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    const models = require('../models');
    return models.user.bulkCreate([
      {
        id: 1,
        email: 'tak@oshiire.to',
        password: '$2b$10$t73WMpPlvyhkWuL.ALWe..OKbU1q1ssR4K5ezVTXLlvaDMtUuAqve',
        salt: '$2b$10$t73WMpPlvyhkWuL.ALWe..'
      },
      {
        id: 2,
        email: 'sho@oshiire.to',
        password: '$2b$10$t73WMpPlvyhkWuL.ALWe..OKbU1q1ssR4K5ezVTXLlvaDMtUuAqve',
        salt: '$2b$10$t73WMpPlvyhkWuL.ALWe..'
      },
      {
        id: 3,
        email: 'shosan@oshiire.to',
        password: '$2b$10$t73WMpPlvyhkWuL.ALWe..OKbU1q1ssR4K5ezVTXLlvaDMtUuAqve',
        salt: '$2b$10$t73WMpPlvyhkWuL.ALWe..'
      }
    ]);
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('users', null, {});
  }
};
図8 passportストラテジの定義
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;

const hashPassword = (password, salt) => {
  var bcrypt = require('bcrypt');
  var hashed = bcrypt.hashSync(password, salt);
  return hashed;
};

passport.use(new LocalStrategy(
  {
    usernameField: 'email',
    passwordField: 'password'
  },
  (username, password, done) => {
    models.user.findOne({ where: { email: username } }).then(user => {
      if (!user)
        return done(null, false, { message: 'Incorrect email.' });
      if (hashPassword(password, user.salt) !== user.password)
        return done(null, false, { message: 'Incorrect password.' });
      return done(null, user.get());
    });
  }
));
図9 セッション連携の関数「serializeUser」と「deserializeUser」
var session = require('express-session');

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  models.user.findById(id).then(user => {
    if (user) {
      done(null, user.get());
    } else {
      done(user.errors, null);
    }
  });
});
図10 ミドルウエアで認証済みか否かの判定
app.use((req, res, next) => {
  if (req.isAuthenticated())
    return next();
  if (req.url === '/' || req.url === '/login')
    return next();
  res.redirect('/');
});

app.use('/', index);
app.use('/books', books);
図11 テストコード(supertest-spec.js)の主要部分
/* eslint-env jasmine */

// routing テスト
const request = require('supertest-session');
const app = require('../app');

//let's set up the data we need to pass to the login method
const userCredentials = {
  email: 'tak@oshiire.to',
  password: 'password'
};
const wrongEmailCredentials = {
  email: 'foo',
  password: 'password'
};
const wrongPasswordCredentials = {
  email: 'tak@oshiire.to',
  password: 'foo'
};

//now let's login the user before we run any tests
const authenticatedUser = request(app);

describe('POST /login', () => {
  it('should redirect to / with unloggined user', (done) => {
    request(app)
      .get('/books/')
      .expect(302)
      .expect('Location', '/', done);
  });
  it('should success login with correct user', (done) => {
    request(app)
      .post('/login')
      .send(userCredentials)
      .expect(302)
      .expect('Location', '/books/', done);
  });
  it('should deny login with wrong email', (done) => {
    request(app)
      .post('/login')
      .send(wrongEmailCredentials)
      .expect(302)
      .expect('Location', '/', done);
  });
  it('should deny login with wrong password', (done) => {
    request(app)
      .post('/login')
      .send(wrongPasswordCredentials)
      .expect(302)
      .expect('Location', '/', done);
  });
});

describe('with Login', () => {
  beforeAll((done) => {
    authenticatedUser
      .post('/login')
      .send(userCredentials)
      .expect(302, done);
  });

  describe('GET /', () => {
    it('respond with http', (done) => {
      authenticatedUser
        .get('/')
        .set('Accept', 'text/html')
        .expect(200, done);
    });
  });
 :
(中略)
 :
});

describe('GET /logout', () => {
  beforeAll((done) => {
    authenticatedUser
      .post('/login')
      .send(userCredentials)
      .expect(302, done);
  });

  it('should go back to login', (done) => {
    authenticatedUser
      .get('/logout')
      .set('Accept', 'text/html')
      .expect(302)
      .expect('Location', '/', done);
  });
});