オフトゥン大好き。

惰眠系プログラマの作業ログで( ˘ω˘ ) スヤァ…

ウェブアプリ開発日誌:Rails5でユーザ登録機能を実装するまでのあれこれ

Railsアプリにユーザ登録・認証を実装するまでの手順を書きます。ここでいうユーザ登録は、よくあるユーザ名とメールアドレスを入力して届いた認証リンクにアクセスするとアクティベートされるタイプのやつです。

環境

  • Ruby 2.3.0
  • Rails5 (5.0.2)
  • devise (4.2.0)
  • omniauth, omniauth-twitter
  • figaro

rails new

プロジェクトディレクトリを作ります。今回はデータベースにMySQLを使うのと、テストにはRSpecを使いたいのでデフォルトのテストは外しておきました。

rails new myapp -T -B -d mysql

付与したオプションは次の通り。

  • -T デフォルトのテストコード生成を外す
  • -B 生成直後のbundleをスキップする
  • -d mysql データベースをMySQLに変更する(デフォルトはPostgreSQL

deviseを導入する

deviseはrailsでユーザ登録・認証機能を実装するgemです。正直ユーザ機能つけるならこれ一択みたいなところがあって、他にもないわけではないですがOAuthやらAPI作成やらとの連携も最も進んでいてわざわざ他のを選ぶ理由がないという感じです。

Gemfileに認証系のgemを追加。

gem 'devise'
gem 'omniauth'
gem 'omniauth-twitter'

bundle install後、bundle exec rails g devise:installでdeviseの設定ファイルを生成します。具体的にはconfig/initializers/devise.rbconfig/locales/devise.en.ymlができますが、それは今のところどうでもよくてその後に表示される手順が重要です。

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

これを一つ一つ順番に実行していきましょう。

Userモデルの作成

認証に用いるモデルを作成します。名前はなんでも構いませんが、特に理由がなければUserが無難だと思います。

$ rails g devise User

できたモデルファイルを変更します。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :timeoutable, :omniauthable, omniauth_providers: [:twitter]
end

また、マイグレーションファイルからコメントアウトをいくつか外してカラムを追加します。

class DeviseCreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

今回はlockableを省きましたが、アカウントのロック機能を利用する場合はこれもコメントアウトを外します。

figaroを導入

omniauthでtwitter認証を導入するのはいいですがAPIのトークンやシークレットをそのままコードに書くのはダメです。あとで消すつもりでも、うっかりコミットしてしまったりしたら最悪です。 運用では環境変数にこの手のものを指定するのが定石ですが、開発環境でいちいち変数をexportするのも面倒なのでfigaroというgemを使います。 これは変数をファイルに書き出しておいて、それを環境変数として読み込んでくれるやつです。したがって、開発環境ではgitignoreでバージョン管理から外れたファイルで、本番では環境変数で指定といった使い分けができます。

gem 'figaro'

bundle installしたらfigaroで必要になるファイルをインストールしましょう。

bundle exec figaro install

これでconfig/application.ymlというファイルが追加され、.gitignoreにそれが追記されたはずです。このapplication.ymlが環境変数を指定するためのファイルになります。

twitter認証を追加する

Twitter Application Managementにアクセスしてアプリケーションの登録を行います。

https://gyazo.com/5ce2926e4e9f9875b04ed82c2a755d0f

APIトークンとシークレットを設定する

入手したトークンとシークレットをapplication.ymlに記述し、それを環境変数として読み込めるようにします。

development:
    TWITTER_API_KEY: '<token>'
    TWITTER_API_SECRET: '<secret>'

config/initializers/devise.rbにomniauthの設定を記述します。figaroが設定した環境変数を読み出すための初期化コードです。

Devise.setup do |config|
  config.omniauth :twitter, ENV['TWITTER_APP_ID'], ENV['TWITTER_APP_SECRET']
end

これでapplication.ymlを別に共有しておけば開発環境でいちいち変数を設定する手間が省けますし、環境別にファイルを分けたりすることもできます。このファイルの共有方法に関しては議論の余地がありますが、少なくともバージョン管理に含めることが間違いなのは明らかなのでそこからの漏洩リスクはこれでどうにかなりました。

OAuth用のマイグレーションを作成する

ここまでやったらあとはbundle exec rake db:migrateを実行します。これでデータベースに認証に必要なテーブルができました。

callback用のコントローラを作る

twitterアプリ作成時に指定したコールバックURLを処理できるようにコントローラを作ります。

$ rails g controller omniauth_callbacks

omniauth_callbacks_controller.rbを以下のように変更する。

class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    @user = User.from_omniauth(request.env["omniauth.auth"].except("extra"))

    if @user.persisted?
      sign_in_and_redirect @user
    else
      session["devise.user_attributes"] = @user.attributes
      redirect_to new_user_registration_url
    end
  end
end

参考文献

No.227 簡単ポーカー 解法

No.227 簡単ポーカー - yukicoderを解いた。

手札の役を作る処理は決定性有限オートマトンに置き換えられる。

開始状態はNO HANDとし、空のスタックを用意してソートした入力リストを先頭から順に積んでいく。ソートは同じ値をグループ化するために行なっているので昇順でも降順でも構わない。

  • 入力リストから取り出した値がスタックtopと同じならそのまま積む
  • 入力リストから取り出した値がスタックtopと異なるならスタックサイズを入力として状態遷移し、スタックを空にしてから値を積む

状態遷移表は次のようになる。TWO PAIRが必要ないのは入力リストから取り出した値がスタックtopと同じであるかぎり積んでいくのでTWO PAIR => FULL HOUSEという遷移はあり得ないため。

# \ s NO HAND ONE PAIR THREE CARD
2 ONE PAIR TWO PAIR FULL HOUSE
3 THREE CARD FULL HOUSE -

(#:スタックサイズ, s:現在の状態)

これを元にパスを作っていくと次のような状態遷移図が出来上がる。

f:id:nukosuke:20161023162936p:plain:w320

これをPerlで実装した。

sub trans {
  my ($state, $stack_size) = @_;
  if    ($state eq "NO HAND"    && $stack_size == 2) { return "ONE PAIR";   }
  elsif ($state eq "NO HAND"    && $stack_size == 3) { return "THREE CARD"; }
  elsif ($state eq "ONE PAIR"   && $stack_size == 2) { return "TWO PAIR";   }
  elsif ($state eq "ONE PAIR"   && $stack_size == 3) { return "FULL HOUSE"; }
  elsif ($state eq "THREE CARD" && $stack_size == 2) { return "FULL HOUSE"; }
  else  { return $state; }
}

my @A = sort { $a <=> $b } map { $_ + 0 } split ' ', <>;
my $state = "NO HAND";
my @stack = ();

foreach (@A) {
  if ($#stack >= 0 && $_ != $stack[$#stack]) {
    $state = trans($state, $#stack+1);
    @stack = ();
  }
  push @stack, $_;
}
print(trans($state, $#stack+1) . "\n");

関連記事

No.231 めぐるはめぐる (1) 解法

No.231 めぐるはめぐる (1) - yukicoderを解いた。
面倒なので解説にあるとおり、一番簡単な解法を選ぶ。

一番効率のいい狩場に6時間行って,可能ならそれ,不可能なら不可能が一番楽です.

6時間で経験値3000000以上を得られるダンジョンが存在するかどうか、存在する場合は何行目のダンジョンかという情報が必要になる。
よって下記のように答えが求まる。

my $N   = <> + 0;
for (1..$N) {
  my ($G, $D) = map { $_ + 0 } split ' ', <>;
  if (($G - 30000 * $D) * 6 >= 3000000) {
    print("YES\n" . "$_\n" x 6);
    exit(0);
  }
}
print "NO\n";

JavaScriptテスト環境セットアップ: Node.js編

Node.jsでテスト書くためのスタックとか設定を書く。
あくまで僕がいつも使ってるやつなので他にもっといいのあったら教えてください。

スタック

Node.jsのテストはだいたい次の4つのパッケージで完結してる。フロントのテストはもっとめんどくさい。

name description
mocha Node.jsのテストランナー
chai BDD/TDDスタイルでテストを書くためのライブラリ
sinon JavaScriptのモックライブラリ
sinon-chai chaiでsinonを使うためのchaiプラグイン
$ npm install mocha chai sinon sinon-chai -D

セットアップ

globalにテスト中に使う変数を入れておく

僕はいつもExpect形式で書くのだけど、そのままだとテストファイルの冒頭に以下のような設定を毎回書かないといけない。

const chai  = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
chai.use(sinonChai);

それは面倒だし、メンテナンス性も悪いので別ファイルにまとめておいて、mocha起動時にそれを読み込ませる。例えば/test/globals.jsというファイルを作ってglobalにexpectを突っ込んでおく。

const chai  = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');

chai.use(sinonChai);
global.expect = chai.expect;



npm testを定義する

作成したglobal.jsをテスト前に読み込むにはpackage.jsonscripts:test項目に次のようにコマンドを割り当てておく。

"scripts": {
    "test:main": "$(npm bin)/mocha './test/**/*.spec.js' -r ./test/globals --recursive"
}

すると'./test/globals.js'がrequireされた状態でテストが開始するので書くファイルにexpectの宣言を書かずに使用できる。
こんな感じで。

const API = require('./api');

describe('create API client', () => {

    const client = new API();

    it('fetch user info successfully', done => {
        const user = client.getUserInfo();
        
        expect(user)
            .to.have.property('name')
            .that.is.a('string');

        done();
    });
});



Travis CI

追加でTravis CIの設定をして自動化する。
とはいってもここまでやるともう雑な.travis.ymlを用意するだけで良い。

language: node_js
node_js:
  - 6
  - 5
  - 4



次はフロント側のテストについて書く。

関連記事

npmプロジェクトを0からはじめるメモ - オフトゥン大好き

homebrew-caskの書き方とpull-requestの出し方

GitHub - caskroom/homebrew-cask: A CLI workflow for the administration of Mac applications distributed as binaries

Mac使いならほとんどの人がお世話になっているHomebrew。 その拡張であるHomebrew Caskは以前なら別途インストールする必要があったが、色々ゴタゴタがあった末に本体に統合された模様。

僕は環境構築をできる限り自動化していてMacのプロビジョニングを行うにあたってはこれがないと結構面倒だ。もしHomebrew Caskのリポジトリに必要なCaskがなければ自分で作ってプルリクを出したら他にも幸せになる人がいるかもしれない。今回Windowsで使っていた3DモデリングソフトがいつのまにかMacに対応していたのでCaskに固めてみた次第。

Caskを作成する

まず最新のHomebrewが導入された状態で次のコマンドを実行する。hogehogeは作成するCaskの名前に読み替えてほしい。

$ brew cask create hogehoge

するとhogehoge.rbができるのでこれに必要な情報を書き加えていく。

cask 'hogehoge' do
  version ''
  sha256 ''

  url ''
  name ''
  homepage ''

  app ''
end
項目 内容
version アプリケーションのバージョンを指定する
sha256 改ざんや破損検知のためSHA256でイメージやアーカイブチェックサムを取る場合、ここにハッシュを入れておく。スキップする場合は:no_checkシンボルを指定する。
url ダウンロードするファイルのURL。CDNなどを利用していてhomepageで指定するドメインと異なる場合は# <ダウンロード先のドメイン> was verified as official when first introduced to the caskというコメントを一行上につけないとLintで弾かれる。
name アプリケーションの名前を指定する。
homepage アプリケーションのホームページを指定する。多くの場合はコーポレートサイトの製品ページなど。
app .appファイルの名前を書いておくと.dmgがマウントされた後にこれが/Applicationにコピーされる。

Macのアプリケーションは様々なインストール手段があるので、appでは対応できないことがある。例えば.dmgの中身が.pkg形式のインストーラだった場合。appではなくpkgを使用する。

また、この場合uninstallの項目にベンダーのIDを指定する必要があるが、これはhomebrew-caskのリポジトリにあるスクリプトで確認できる。

$ ./developer/bin/list_recent_pkg_ids
com.tetraface.Metasequoia.pkg
com.microsoft.OneDrive-mac
com.autodesk.mas.123ddesign
com.agilebits.onepassword-osx
com.microsoft.office.all.core.pkg.14.6.8.update
com.microsoft.office.all.dcc.pkg.14.6.8.update
com.microsoft.office.all.excel.pkg.14.6.8.update
com.microsoft.office.all.fix_permissions.pkg.14.6.8
com.microsoft.office.all.outlook.pkg.14.6.8.update
com.microsoft.office.all.powerpoint.pkg.14.6.8.update

最終的には以下のような状態になる。

cask 'hogehoge' do
  version '1.0.0'
  sha256 '0cd88d99379bdf8dfa42fd13ee970d8921f6d5eb3d892f76e4a7c76d68dc9e6d'

  # cdn.hogehoge.net was verified as official when first introduced to the cask
  url "https://cdn.hogehoge.net/download/Hogehoge-#{version}-Installer.dmg"
  name 'Hogehoge'
  homepage 'https://www.hogehoge.com/'

  pkg "Hogehoge-#{version}-Installer.pkg"

  uninstall pkgutil: 'com.pkg.hogehoge'
end

プルリクを出す

結構ガイドラインが細かったりするので目を通しておいた方がいいが、プルリク時のテンプレートにチェックボックスが用意されていて基本的にはこれを満たしていれば問題ない。

  • brew cask audit --download {{cask_file}}でエラーが出ないかどうか
  • brew cask style --fix {{cask_file}}でエラーが出ないかどうか
  • コミットメッセージにCaskの名前とバージョンを記載しているか
  • token referenceに沿って名前を付けているか
  • brew cask install {{cask_file}}でエラーが出ないかどうか
  • brew cask uninstall {{cask_file}}が正常に動くかどうか
  • すでに同じアプリケーションのプルリクが出ていたり過去に却下されていないか

本体のリポジトリにマージされるとbrew cask install hogehogeでインストールできるようになるので、ansibleのplaybookには

homebrew_cask_packages:
  - { name: hogehoge }

とか書いておくと新型に買い換えたときの環境構築で幸せになれる。