N予備校プログラミングコースで作るアプリに機能追加していく。

どうもこんにちは
簡単に自己紹介すると自分は理系の大学生3年生で、今年N予備校のプログラミングコースをきっかけに本格的にプログラミングを始めたものです。入門コースを一周した後も、週末にプログランミングするようになったのは他でもないN予備校のおかげです。N高生ではありませんがN予備校生としてアドベントカレンダーを書かせてもらいます。

はじめに

内容はN予備校のプログラミングコース4章で作ったwebアプリ「予定調整くん」に手を加えるというものです。
プログラミングコースは基本的にコードを指示どおりに書いて手を動かして学んでいくという形式ですが、進めていくうちにこんなものを作ってみたいというアイデアが出てくることがありました。しかし、一からプログラムを書くことしてみても、なかなか厳しいというのが現実。そこで、理解できたプログラムを改変したり、機能拡張することを考えました。
こうしたとき、どういう手順でやり、どうつまずいたなどの知見を共有できればと思います。
ちなみに自分が手を加えて開発し、公開しているものはこちら
ソースコードはこちらです。
今回は予定調節くんに Facebook 認証でログインできる機能を例に説明していきます。

環境

  • プログラミング入門コース同様 仮想環境の Ubuntu 上で開発

Facebook 認証を実装

N予備校のプログラミングコース最終章の認証部分を編集します。
入門コースでは app.js 内に認証の処理の全てを記述していますが、認証関係のコードが増えてしまうのであとで編集しやすいように routes/auth.js というファイルに切り分けました。

auth.js
    'use strict';
    const express = require('express');
    const router = express.Router();
    const passport = require('passport');
    const GitHubStrategy = require('passport-github2').Strategy;
    const FacebookStrategy = require('passport-facebook').Strategy;
    const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || '${GITHUB_CLIENT_ID}';
    const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || '${GITHUB_CLIENT_SECRET}';
    const FACEBOOK_APP_ID = process.env.FACEBOOK_APP_ID || '${FACEBOOK_APP_ID}';
    const FACEBOOK_APP_SECRET = process.env.FACEBOOK_APP_SECRET || '${FACEBOOK_APP_SECRET}';
    const User = require('../models/user');

    githubAuth();
    router.get('/github', passport.authenticate('github', { scope: ['user:email'] }),
      (req, res) => {});
    router.get('/github/callback',
      passport.authenticate('github', { failureRedirect: '/login' }),
      (req, res) => {
        authRedirect(req, res);
      });

    facebookAuth();
    router.get('/facebook',
      passport.authenticate('facebook', { scope: ['email'] }),
      (req, res) => {});
    router.get('/facebook/callback',
      passport.authenticate('facebook', { failureRedirect: '/login' }),
      (req, res) => {
        authRedirect(req, res);
      });

    function authRedirect(req, res) {
        var loginFrom = req.cookies.loginFrom;
        // オープンリダイレクタ脆弱性対策
        if (loginFrom &&
         loginFrom.indexOf('http://') < 0 &&
         loginFrom.indexOf('https://') < 0) {
          res.clearCookie('loginFrom');
          res.redirect(loginFrom);
        } else {
          res.redirect('/');
        }
    }

    function githubAuth() {
      handleSession();
      passport.use(new GitHubStrategy({
          clientID: GITHUB_CLIENT_ID,
          clientSecret: GITHUB_CLIENT_SECRET,
          callbackURL: process.env.HEROKU_URL ? process.env.HEROKU_URL + 'auth/github/callback' : 'http://localhost:8000/auth/github/callback'
        },
        (accessToken, refreshToken, profile, done) => {
          process.nextTick(() => {
            User.upsert({
              userId: profile.id,
              username: profile.username
            }).then(() => {
              done(null, profile);
            });
          });
        }
      ));
    }

    function facebookAuth() {
      handleSession();
      passport.use(new FacebookStrategy({
          clientID: FACEBOOK_APP_ID,
          clientSecret: FACEBOOK_APP_SECRET,
          callbackURL: process.env.HEROKU_URL ? process.env.HEROKU_URL + 'auth/facebook/callback' : 'http://localhost:8000/auth/facebook/callback'
        },
        (accessToken, refreshToken, profile, done) => {
          process.nextTick(() => {
            User.upsert({
              userId: profile.id,
              username: profile.displayName
            }).then(() => {
              done(null, profile);
            });
          });
        }
      ));
    }

    function handleSession() {
      passport.serializeUser((user, done) => {
        done(null, user);
      });
      passport.deserializeUser((obj, done) => {
        done(null, obj);
      });
    }
    module.exports = router;

簡単に要所を説明していきます。

const FacebookStrategy = require('passport-facebook').Strategy;

Facebook 認証を実装するには Github 認証と同じく passport の passport-facebook を利用して実装できます。これはpassport のドキュメントからわかります。

const FACEBOOK_APP_ID = process.env.FACEBOOK_APP_ID || '${FACEBOOK_APP_ID}';
const FACEBOOK_APP_SECRET = process.env.FACEBOOK_APP_SECRET || '${FACEBOOK_APP_SECRET}';

Facebook 認証をはじめ、シェアボタンなどを実装するには Facebook の開発者ページ
でアプリを登録する必要があります。なおこの際 Facebook アカウントが必要なのでまだ持っていない人は事前に作る必要があります。

このサイトのメニューのプルダウンからアプリを作ることができます。

アプリを作れたらダッシュボードから ${FACEBOOK_APP_ID}${FACEBOOK_APP_SECRET} が所得できます。

また Facebook ログイン機能を追加し、リダイレクト先の URL などを設定します。

開発環境のときは https://localhost:8000/auth/facebook/callback などリダイレクト先のアドレスをわり当てておきます。ちなみにFacebook 認証は開発環境と本番環境では混乱をさけるために別のアプリとしてそれぞれ登録しておく方がいいでしょう。

    function facebookAuth() {
      handleSession();
      passport.use(new FacebookStrategy({
          clientID: FACEBOOK_APP_ID,
          clientSecret: FACEBOOK_APP_SECRET,
          callbackURL: process.env.HEROKU_URL ? process.env.HEROKU_URL + 'auth/facebook/callback' : 'http://localhost:8000/auth/facebook/callback'
        },
        (accessToken, refreshToken, profile, done) => {
          process.nextTick(() => {
            User.upsert({
              userId: profile.id,
              username: profile.displayName
            }).then(() => {
              done(null, profile);
            });
          });
        }
      ));
    }

あとは Github 認証とほぼ同様に passport を使って実装できます。

使い方は node_modules を見れば profile にどんな値が格納されているのか確認することができるので参考になるでしょう。
また今回は解説しませんがこの画像にある provider を使って facebook のときの処理、github の処理をそれぞれ制御したいときなどにも使えます。
これで Facebook ログインが正常に動作するかに思えますが、下記のようなエラー文が出ます。

エラー文 を見てみるとどうやらupsert()を実行しているときに out of range つまり今設定しているデータ型では Facebook がレスポンスする userId を格納できないようです。ここで Sequelizeのドキュメント を探すと

があります。 どうやら Sequelize には INTEGER の他に設定できる整数の値があるようです BIGINT というデータ型が用意されているようです。(厳密には Postgres にですが) そうならばとモデル部分を書き換えてみます。

modeles/user.js
    'use strict';
    const loader = require('./sequelize-loader');
    const Sequelize = loader.Sequelize;
    const User = loader.database.define('users', {
      userId: {
        type: Sequelize.BIGINT,
        primaryKey: true,
        allowNull: false
      },
      username: {
        type: Sequelize.STRING,
        allowNull: false
      }
    }, {
      freezeTableName: true,
      timestamps: false
    });
    module.exports = User;

models/user.js の INTEGER を BIGINT に変更してみました。
省略しますが、他の userId を外部キーとしているモデルにも変更を加える必要があります。
ちなみに自分は dababase を作り直すのを忘れていてかなり時間を無駄にしました。
drop database schedule_arranger; create database schedule_arranger;でデータベースのスキーマを変更したら必ず database を作り直しましょう。

無事データベースに格納できました。これで Facebook 認証は実装できました。

なぜ integer 型では out of range だったのか

integer 型ではなぜだめなのかと言うと、これは integer 型が 10 桁程度まで、正確に言うと(-2147483648 から +2147483647)の 4 バイト までしか表現できないためと考えられます。 Github の userId は 8桁 でこれに収まりますが、Facebook の userId は15 桁 収まりません。そのためエラー文が出ました。bigint 型はおよそ 19 桁(8バイト)まで扱うことができるため格納できるということです。
あとこれは完全に余談ですが、この 4 byte( = 32 bit)、を 10 進数で直感的に考えたいときがたまにあります。いちいち計算してられませんから覚えるときに 21.4 億なので中国とインドの人口の合計と覚えていたのですが、2016年の中国とインドの人口の合計は 27億人 だそうです。すごいスピードで人口が増えている…もうこれは通用しないんですね…
integer : 4byte = 32bit ≒ 約21.4億
bigint: 8byte = 32bit

機能追加していくには

一例として Facebook 認証を追加して行く過程を紹介しました。段階としては
Github 以外でも認証できる? => Passportのドキュメントを読む => できそう! => out of range のエラー文がでる。 => そうか Facebookの場合は userId の桁数が違うんだ! => sequelizeのドキュメントを読む => 直す => 実装完了
一般化すると
こういうことができそう? => ドキュメントを読む => できそう! => 実装 => エラー文がでる。 => こう言う場合のことを考えれてなかった! => 直す => 実装完了
という流れになると思います。
自分はここで重要なのがドキュメントを読むことエラー文と向き合うことだと思っています。ここで妥協すれば無駄に時間を浪費する可能性が高まるからです。

ドキュメントを読む

まったくの一から実装するなら言語の機能の根本的理解や数学の勉強をしなければならないかもしれませんが、アプリを実装するとなるとライブラリを使うことがほとんどです。ライブラリの作者達も自身が解決したい問題と、こういう風に使ってほしいという想いが当然あるはずですから、それがドキュメントに書いているはず。私としては、それらをないがしろにしていたという反省があります。最初からヘッドラインや概要すら読まずに欲しい情報だけ API リファレンスを ctrl + f で検索して適当に読んで、つまずきまた読み直して勘違いしていることに気づくといった感じです。本来はそのライブラリがどういう思想で何を解決したいのか読み取ることにまずは務めるべきでした。なのでまずは README や概要を読むようにしています。それでも文章を雑に読んでしまうことが現在も課題ですが、それらは意識することで多少なりともマシになるかと思います。(これは自分だけかもしれません。)

エラー文を読む

赤い字で長々と英文が書いてあったら読む気が失せますよね。私もエラー文が出たら投げ出していました。しかしやるべきことは当然ながらまずはエラー文をきちんと読むことです。それでもわからない場合はググりますが、ここで理解できていないコードをコピペして直すということをしてしまうと、あとで何をしているのかさっぱりわからなくなり詰むということもしばしば。エラー文に対する解決策が書かれていても、どんな問題に対してどう対処しているのか理解した上でコピペなり改変して対処するというのがいいかと思います。

ドキュメントもエラー文もだいたいは英語

自分は二つの chrome 拡張を使って全体を掴んでよくわからないところは適宜訳して読んでいます。

https://insttranslate.com/
https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb?hl=ja

もちろん、英語をストレートにバリバリ読めることに越したことはありません。それを目指してN予備校中久喜先生の英文読解を受講しています、英文の音声もついていて大変わかりやすいです。とはいえドキュメントは読めなくても読むしかないので道具を使えるところは使っていきます。また翻訳や日本語でライブラリについて解説された技術書なども出版されているのでお金の力で解決すると手もあるかと。

できる人に質問する

「エラー文が乗り越えられない」、「この機能実装できそうだと思ったけどうまく行かない」といったとき人に聞くという最終手段があります。最終手段と言ったのは働く現場ならともかく、自分の時間であれば実力をつけるためにできるだけ考え抜いた方がいいと思うからです。近くにできる人がいない場合は難しいかもしれませんが、幸いN予備校にはフォーラム(Q&A)という機能があり質問ができます。ですが、その前に自分は下記を確認するようにしています。

質問する前に

  • 文法は間違っていないか
  • タイポしていないか
  • エラー文をしっかり読んだか
  • 教材どうりにやれているか (教材についてなら)

質問するときに

  • そもそも何をしているのか正確に説明する
  • 不可解な点はどこか、あるいは思い当たる原因をかく
  • 原因がわかる場合は試みた解決策をかく
  • 自分の環境をかく(バージョンやマシンなど)

おそらく最低限これを抑えていれば快く返事してくださる方が現れるはずです。
また、文法チェックなどは エディタの拡張機能などを利用するという手もあります。VSCode であれば組み込みの機能もありますし、例えば吉村先生(@sifue)が書いてらっしゃるこの記事も参考になると思います。

まとめ

今回は当たり前な人にとってはあたりまえかもしれませんし、意識せずとも特に問題なくやれる人もいるかと思いますが、機能追加する際の
こういうことができそう? => ドキュメントを読む => できそう! => 実装 => エラー文がでる。 => こう言う場合のことを考えれてなかった! => 直す => 実装完了
の流れを説明してみました。もし「自分はこんな風に工夫してる!」とか「これ実は悪手じゃない?」みたいなのがあればぜひコメントください。
以上
最後まで読んでいただきありがとうございます!