devise token auth routing error

devise token authというトークンベースで認証を行えるgemを使用していたのですが、
routing errorが出てきました

[toc]

auth/password/edit?

環境

  • railsのAPIサーバー
  • Angular2で作ったクライアント

エラー内容

パスワードをリセットするためのエンドポイントにリクエストをして、
送信されたメールアドレスのリンクから、変更するためのコンポーネントに移動する

という流れを想定していたのですが、

routing error"/auth/password/edit**********" 

このようなエラーが出てきました

解決策

問題だったのは、APIサーバの
config/environment/development.rbの設定でした

普段は localhost:3000 で使用していたのですが、
今日は localhost:4000 にしていて、ポートの設定が間違っていたみたいです

本当に凡ミスでしたが、4時間ほどかかってようやくミスに気がつきました。。。

みなさんきをつけてください。。。。

参考にした記事

route not found from link in email password reset

devise token auth を使って簡単に早くAPIを作る 1

※ちょくちょくGithubで公開してもらえませんか?的なことを聞かれるので、Bitbuckeのアカウントでコードを公開いたしました。ミスなどがあったらすみません。時間があるときに直します。
devise-token-auth 完成物のリンク

関連記事
全て連続しているので上から順に進めていっていただけるとわかりやすいと思います
devise token auth を使って簡単に早くAPIを作る1( api作成 ) 今回はここ
devise token auth を使って簡単に早くAPIを作る2 ( jsonの出力 )
devise token auth api をAngular4 でUIを実装する1 ( angularのセットアップ )
devise token auth api をAngular4 でUIを実装する2 ( ログインを繋げる )
devise token auth api をAngular4 でUIを実装する3 ( 新規登録 )
devise token auth api をAngular4 でUIを実装する4 ( ログインしやすくする)

通常はdeviseでログイン機能を構築することが多いと思いますが、
devise token auth を使用すると、トークンベースで認証が可能となるので、
iOSやアンドロイドアプリを作りたいといった場合に結構重宝しています

複数回に分けて簡単にトークン認証を構築してから、
最後はアクセス制限を使用してみたいと思います!

# rails version
rails 5系

ベースを作る

まずはrails new します

$ rails new devise_token_auth --api -d mysql

次にGemfileをいじります

source 'https://rubygems.org'

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end

# 修正依頼がありましたので、追加しました。お手数おかけしました。
gem 'omniauth'

gem 'rails', '~> 5.0.1'
gem 'mysql2', '>= 0.3.18', '< 0.5'
gem 'puma', '~> 3.0'

# トークンベースの認証を行うためのgem
gem 'devise'
gem 'devise_token_auth'

# jsonを扱うためのgem
gem 'jbuilder'

# クロスドメイン対策のgem
gem 'rack-cors'

group :development, :test do
  gem 'byebug', platform: :mri
end

group :development do
  gem 'listen', '~> 3.0.5'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

次はbundle installします
ついでにデータベースも作っておきます

$ bundle install
$ rake db:create

devise token auth

devise token auth をインストールします

$ rails g devise_token_auth:install User auth

コマンドを走らせるとマイグレーションファイルが生成されるので、それを編集します
今回は、ユーザーが新規に登録する際に、

  • 企業名
  • メールアドレス
  • 名前

を入力してもらえるようにカラムを追加しておきます

twitterやfacebookでログインする機能は今回は触れないのでコメントします
メールを送信して登録する、といった機能もつけることはしないので、
モデルは以下のように編集してください

class User < ActiveRecord::Base
  # Include default devise modules.
  devise :database_authenticatable, :registerable,
          :recoverable, :rememberable, :trackable, :validatable
          # :confirmable, :omniauthable
  include DeviseTokenAuth::Concerns::User
end

次にカラムを追加するために、devise token authのマイグレーションファイルを修正します

class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table(:users) do |t|
      ## Required
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""

      ## Database authenticatable
      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


      ##ここも修正!

      ## User Info
      t.string :company
      t.string :email
      t.string :name

      ## Tokens
      t.text :tokens

      t.timestamps
    end

    add_index :users, :email,                unique: true
    add_index :users, [:uid, :provider],     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

さて次はマイグレーションを走らせます
ルートも確認しておきます

$ rake db:migrate

$ rake routes

  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /auth/sign_in(.:format)        devise_token_auth/sessions#new
            user_session POST   /auth/sign_in(.:format)        devise_token_auth/sessions#create
    destroy_user_session DELETE /auth/sign_out(.:format)       devise_token_auth/sessions#destroy
       new_user_password GET    /auth/password/new(.:format)   devise_token_auth/passwords#new
      edit_user_password GET    /auth/password/edit(.:format)  devise_token_auth/passwords#edit
           user_password PATCH  /auth/password(.:format)       devise_token_auth/passwords#update
                         PUT    /auth/password(.:format)       devise_token_auth/passwords#update
                         POST   /auth/password(.:format)       devise_token_auth/passwords#create
cancel_user_registration GET    /auth/cancel(.:format)         devise_token_auth/registrations#cancel
   new_user_registration GET    /auth/sign_up(.:format)        devise_token_auth/registrations#new
  edit_user_registration GET    /auth/edit(.:format)           devise_token_auth/registrations#edit
       user_registration PATCH  /auth(.:format)                devise_token_auth/registrations#update
                         PUT    /auth(.:format)                devise_token_auth/registrations#update
                         DELETE /auth(.:format)                devise_token_auth/registrations#destroy
                         POST   /auth(.:format)                devise_token_auth/registrations#create
     auth_validate_token GET    /auth/validate_token(.:format) devise_token_auth/token_validations#validate_token

このままだとデフォルトのコントローラーが使用されてしまうので、オーバーライドして、

  • :company(企業名)

を登録できるように修正します

controllersディレクトリに api ディレクトリを作成して、さらにauthディレクトリの中にregistrations_controllerを作成します

$ rails g controller api/auth/registrations

内容は以下のようにします

module Api
  module Auth
    class RegistrationsController < DeviseTokenAuth::RegistrationsController

      private
      # :company( 企業名 )を追加できるようにpravateメソッドに修正を加える
      def sign_up_params
        params.permit(:name, :email, :company, :password, :password_confirmation)
      end

      def account_update_params
        params.permit(:name, :email, :company)
      end

    end
  end
end

ルートを修正して、デフォルトのコントローラーではなく、継承したコントローラーを使えるようにします

Rails.application.routes.draw do

  namespace :api do
    mount_devise_token_auth_for 'User', at: 'auth', controllers: {
        registrations: 'api/auth/registrations'
    }
  end

end

rake routesして、 api/auth/registrations# **** が使われていたらOKです!

$ rake routes
                      Prefix Verb   URI Pattern                        Controller#Action
        new_api_user_session GET    /api/auth/sign_in(.:format)        devise_token_auth/sessions#new
            api_user_session POST   /api/auth/sign_in(.:format)        devise_token_auth/sessions#create
    destroy_api_user_session DELETE /api/auth/sign_out(.:format)       devise_token_auth/sessions#destroy
       new_api_user_password GET    /api/auth/password/new(.:format)   devise_token_auth/passwords#new
      edit_api_user_password GET    /api/auth/password/edit(.:format)  devise_token_auth/passwords#edit
           api_user_password PATCH  /api/auth/password(.:format)       devise_token_auth/passwords#update
                             PUT    /api/auth/password(.:format)       devise_token_auth/passwords#update
                             POST   /api/auth/password(.:format)       devise_token_auth/passwords#create
# ここを確認!
cancel_api_user_registration GET    /api/auth/cancel(.:format)         api/auth/registrations#cancel
   new_api_user_registration GET    /api/auth/sign_up(.:format)        api/auth/registrations#new
  edit_api_user_registration GET    /api/auth/edit(.:format)           api/auth/registrations#edit
       api_user_registration PATCH  /api/auth(.:format)                api/auth/registrations#update
                             PUT    /api/auth(.:format)                api/auth/registrations#update
                             DELETE /api/auth(.:format)                api/auth/registrations#destroy
                             POST   /api/auth(.:format)                api/auth/registrations#create
     api_auth_validate_token GET    /api/auth/validate_token(.:format) devise_token_auth/token_validations#validate_token

これで、一応は完成です!

initializeの設定

デフォルトだと、毎回トークンを変更しないと認証しないように設定されていますので、lifespanを1ヶ月有効にします

DeviseTokenAuth.setup do |config|

  # config.change_headers_on_each_request を true にしていると、
  # リクエストごとに token を新しくする必要がある という設定になってしまう
  # 毎回トークンを変更するのは手間なので false にしておく
  # config.token_lifspan はトークンの有効期限
  # セキュリティを重視するなら短めの設定にするなど、ここは個人の判断で
  
  config.change_headers_on_each_request = false
  config.token_lifespan = 1.month

  config.headers_names = {:'access-token' => 'access-token',
                          :'client' => 'client',
                          :'expiry' => 'expiry',
                          :'uid' => 'uid',
                          :'token-type' => 'token-type' }
end

postmanを使って、試してみる

postmanはjsonベースでルートやコントローラーのレスポンスを確認できる便利なツールです

Postman

登録できるか確認する

localhost:3000/api/auth に以下の内容でPOSTします

{
    "company": "株式会社せんべい",
    "name": "せんべい太郎",
    "email": "admin@example.com",
    "password": "11111111"
}

content-typeでapplication/jsonを指定しないとエラーが出てしまうので、指定します

エラーなどが起きなければこんな感じで返ってきます

ログイン

ログインは
localhost:3000/api/auth/sign_inにPOSTします
ログインもcontent-typeを指定してやります

ちなみに、ログインして返ってくる情報がとても重要なのでそれは後述します

{
    "email": "admin@example.com",
    "password": "11111111",
    "password_confirmation": "11111111"
}

responseのheaderの確認( ログインして返って来る情報 )
deviseなどの認証をするとなると、ほとんどの場合がログイン制限をかけたい、コンテンツを制限したいといった理由で使用する事が多いと思います

ログインして返ってきた

access-token ->
FqK_4zPogpbaAgc1rEZaVA

client ->
CP7XfS16PGFjk_d7P-87gA

uid ->
admin@example.com

この3つをheaderに追加してリクエストする事でdeviseと同じように制限をかけたりパスワードの変更等に使用できます

headersの部分を見ると詳しい内容を確認できます↓

パスワードの変更

パスワードの変更方法は、ログインして返ってきたこの3つをheaderに付与することで可能になります
※それぞれの値が異なりますが、ログインするたびに変わるものなので気にしないでください

access-token ->
FqK_4zPogpbaAgc1rEZaVA

client ->
CP7XfS16PGFjk_d7P-87gA

uid ->
admin@example.com

localhost:3000/api/auth/password に PUTします
送信する値は新しいパスワードと確認用の新しいパスワードです

問題なく変更されれば、以下の様なレスポンスが返ってきます

ユーザー情報の変更

ユーザー情報もパスワードの変更と同じで、headerにログインした際に発行された情報を付与します

access-token ->
FqK_4zPogpbaAgc1rEZaVA

client ->
CP7XfS16PGFjk_d7P-87gA

uid ->
admin@example.com

名前や企業名などを
localhost:3000/api/auth に PUTして変更します

うまくいってればこんな感じになってると思います!

パスワードを忘れた際の変更処理

Webサービスを利用していると、パスワードを忘れてしまったりすることがありますがdevise token authはとても簡単に実装することができます

変更するための処理は localhost:3000/api/auth/password に postします
※メール送信の設定をしていないとこの機能は使えないので、まずはメールを送信することができる様にしてから進んでください

# 送るデータは emailと メールに記載される redirect_url の2つです
# redirect_urlは送信されたメールのリンクをクリックした際に飛ばされるリンクを指定します
# 通常であれば、password変更ページに飛ばすのがいいでしょう
{
	"email":"admin@example.com",
	"redirect_url": "http://localhost:3000"
}

上記をPOSTすると登録してあるアドレスにメールが送信されるので、受信したメールの中に記載されているリンクをクリックします

Hello world admin@example.com

Someone has requested a link to change your password. You can do this through the link below.

Change my password # ここをクリック

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

すると、 redirect_url のパラメーターで指定したリンクに飛ばされ、リンクにパスワード変更に必要である

  • access-token
  • client
  • uid

が付与されています

※ここから先はパスワード変更と全く同じです!。パスワードをリセットするためにはaccess-token、client、uidの3つが必要ですが、パスワードを忘れてしまってはどうしようもありません。なので、ユーザーが登録した情報(email)と同じデータがあればそのemailにredirect_urlを付与して送信することでユーザーは情報を変更するために必要な3つのデータが生成され再び修正できるようになる、というロジックです。ただ、セキュリティなどを考えると、emailだけではちょっと甘い感じがするので「秘密の質問」や「親の旧姓」といった項目をパスしたらemailを飛ばすといったクッションを入れてやるとなおいいですね

# リンクにはこんな感じで長い文字列が並んでいます
http://localhost:3000/?client_id=dthD_VfdGCTE49RRPeagVg&config=default&expiry=1498908383&reset_password=true&token=-CZlEHQHUczqobr4kkRWTw&uid=admin%40example.com


access-token ->
-CZlEHQHUczqobr4kkRWTw

client -> 
dthD_VfdGCTE49RRPeagVg

uid ->
自分のアドレス

# をパスワード変更でPUTする

リンクから得られる情報を使ってパスワードを変更してみます

これで無事に変更できます

発展編

以下の記事が発展編です
今回devise token authで作ったAPIをAngular4で繋げる準備をします
devise token auth を使って簡単に早くAPIを作る 2

rails rspec expected: 81.3 (#) got: “81.3” (compared using ==)

rspecでテストを書いていたところ、タイトルのエラーが出てきました

どうやら、rbuy のオブジェクトと、データベースの精度の違いによるもののようです

describe 'GET #show' do
    before do
      @product = create(:product)
      get "/products/#{@product.id}", format: :json
    end
    it 'returns 200 status' do
      expect(response).to be_success
      expect(response.status).to eq(200)
    end
    it 'contains body' do
      json = JSON.parse(response.body)
      expect(json['data']['id']).to eq(@product.id)
      expect(json['data']['height'].to_s).to eq(@product.height.to_s)
      expect(json['data']['width'].to_s).to eq(@product.width.to_s)
      expect(json['data']['depth'].to_s).to eq(@product.depth.to_s)
      expect(json['data']['description']).to eq(@product.description)
    end
  end

参考にした記事

Trouble comparing time with RSpec

devise_token_auth 422 Unprocessable

devise_token_authを使って、トークンベースの認証機能を実装していたら、タイトルのエラーが出てきました。。。

2日ほど訳が分からず悩んでいました。。。。。

 changePassword(body) {
    let headers = new Headers({
      'access-token': this.tokenInfo(),
      'uid': this.uidInfo(),
      'client': this.clientInfo()
    });
    let options = new RequestOptions({headers: headers});
    return this.http.put(this.url + '/auth/password', body, options).subscribe((response) => {
      console.log(response.json());
    });
  }

しかし、よくよく考えてみたら、ヘッダに

  • accecc-token
  • client
  • uid

の3つだけでなく、content-typeもいるんじゃね?ってなって加えてみたらいけました

 changePassword(body) {
    let headers = new Headers({
      'Content-Type': 'application/json',
      'access-token': this.tokenInfo(),
      'uid': this.uidInfo(),
      'client': this.clientInfo()
    });
    let options = new RequestOptions({headers: headers});
    return this.http.put(this.url + '/auth/password', body, options).subscribe((response) => {
      console.log(response.json());
    });
  }

Angular2 TypeError: Cannot set property ‘stack’ of undefined

Angularを使っていたら、タイトルのエラーが出てきました

どうやら、package.jsonの zone.js のバージョンによる影響っぽいです
以下のコードのように、 0.7.2 で npm install してあげたら治りました!

{
  "name": "angular-auth",
  "version": "0.0.0",
  "license": "MIT",
  "angular-cli": {},
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "lint": "tslint \"src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update --standalone false --gecko false",
    "e2e": "protractor"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "^2.3.1",
    "@angular/compiler": "^2.3.1",
    "@angular/core": "^2.3.1",
    "@angular/forms": "^2.3.1",
    "@angular/http": "^2.3.1",
    "@angular/platform-browser": "^2.3.1",
    "@angular/platform-browser-dynamic": "^2.3.1",
    "@angular/router": "^3.3.1",
    "core-js": "^2.4.1",
    "rxjs": "^5.0.1",
    "ts-helpers": "^1.1.1",
    "zone.js": "0.7.2"
  },
  "devDependencies": {
    "@angular/compiler-cli": "^2.3.1",
    "@types/jasmine": "2.5.38",
    "@types/node": "^6.0.42",
    "angular-cli": "1.0.0-beta.24",
    "codelyzer": "~2.0.0-beta.1",
    "jasmine-core": "2.5.2",
    "jasmine-spec-reporter": "2.5.0",
    "karma": "1.2.0",
    "karma-chrome-launcher": "^2.0.0",
    "karma-cli": "^1.0.1",
    "karma-jasmine": "^1.0.2",
    "karma-remap-istanbul": "^0.2.1",
    "protractor": "~4.0.13",
    "ts-node": "1.2.1",
    "tslint": "^4.0.2",
    "typescript": "~2.0.3"
  }
}

参考にしたサイト

angular2 TypeError: Cannot set property ‘name’ of undefined

Active admin、:showでarrayをループさせる方法

個人的にactive adminが好きなので仕事でも利用しています
ちょこっとカスタマイズしたので、忘れないようにメモメモ

[toc]

ループさせる方法

めっちゃ簡単です
コードは以下のようになります

  show do
    attributes_table do
      row :name
      row :price
      row :tags do
        product.tags.collect { |n| n.name }.join(', ')
      end
  end

ちなみにモデルの関係性は
product model と tag model を多対多でアソシエーションしています

homebrewをいじったら Mysql2::Error: Can’t read dir of (errno: 13 – Permission denied): SHOW TABLES LIKE ‘schema_migrations’) が出て来て焦った

[toc]

原因

phpを使う必要があってインストールしました
ちょこっとphpを書いてrubyのプロジェクトに戻ろうと思ったら、
タイトルのエラーが出てしままいました

phpをインストールした際に権限がちょこっと変わっていたみたいで、
それが原因っぽいです

解決策

まずは使用しているmysqlのディレクトリに移動します
そしてターミナルで以下のコマンドを叩きます

$ cd /usr/local/var/mysql
$ ls -l
drwx------    3 ユーザー名  wheel       102 12  1 18:26 *********************
drwx------    3 ユーザー名  wheel       102 12  1 18:26 *********************
drwx------   13 ユーザー名  wheel       442 12  1 15:50 ********************
drwx------    3 ユーザー名  wheel       102 12  1 15:10 ********************

もし、上のように
ユーザー名 wheel と返って来たら、
_mysql wheel に権限を変える必要があります

$ sudo chown -R _mysql:wheel /usr/local/var/mysql
$ ls -l
drwx------    3 _mysql  wheel       102 12  1 18:26 *********************
drwx------    3 _mysql  wheel       102 12  1 18:26 *********************
drwx------   13 _mysql  wheel       442 12  1 15:50 *********************
drwx------    3 _mysql  wheel       102 12  1 15:10 *********************
drwx------    7 _mysql  wheel       238 10 11 11:45 *********************

これでOKです!

参考一覧

brew updateしたらMysql2::Error: Can’t read dir of 〜で怒られた話

yajl-rubyを使ってみた

railsで構築したAPIを少しでも早くできればと思い、yajl-rubyというgemを使ってみました

[toc]

yajl-rubyについて

yajl-rubyはjbuilderを高速化することができるgemです

JSONをjbuilderで出力しているのであれば、やってみる価値はあるかと思います

Faster JSON backends

Jbuilder uses MultiJson, which by default will use the JSON gem. That gem is currently tangled with ActiveSupport’s all-Ruby #to_json implementation, which is slow (fixed in Rails >= 4.1). For faster Jbuilder rendering, you can specify something like the Yajl JSON generator instead. You’ll need to include the yajl-ruby gem in your Gemfile and then set the following configuration for MultiJson:

導入方法

導入はとても簡単です

まずはGemfileに以下の行を追加して bundle install します

gem 'yajl-ruby', require: 'yajl'

次に config/initializers にmulti_json.rb 以下の内容で作成

require 'multi_json'
MultiJson.use :yajl

最後に、ちゃんと反映されているか rails cinsole で確認してみましょう

$ rails console
[1] pry(main)> MultiJson.engine
=> MultiJson::Adapters::Yajl

このように、MultiJson::Adapters::Yajl と返ってこれば反映されています

rails/jbuilder
brianmario/yajl-ruby
how to use jbuilder with yajl

効果

実際に導入してみての効果ですが、若干早くなりました
そもそもrailsは早く開発できることがメリットなので、劇的なスピードアップは難しいとは思いますが、
yajl-rubyなどのgemを使ったりして少しずつ改善していきたいですね!

Active adminでCSSのコンフリクトが起こった際の対処法

active adminを導入すると
assets/javascripts/active_admin.js.coffeeと
assets/stylesheets/active_admin.scssがそれぞれ生成されます

この2つのファイルは既存のファイルと競合する場合が多いので、
vendor ディレクトリに移動させておくと、後々悩まされることがないので、おすすめです!

ArgumentError in Admin::Products#show wrong number of arguments (given 1, expected 0)

active adminを導入して、超絶はまってしまった。

rgumentError in Admin::Products#show
    Showing /Users//.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/activeadmin-4f494073c6c0/app/views/active_admin/resource/show.html.arb where line #2 raised:

Extracted source (around line #48):    
wrong number of arguments (given 1, expected 0)
          temp_method = "__temp__#{safe_name}"
              ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
              @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
              @module.instance_method temp_method
            end
          end

tempなんとかが・・・・みたいなエラーが出てきて、今まで一度も見たことがなかったからものすごい時間を使ってしまった

結論を言ってしまえば、予約語を使っていたからである

カラム名に methodという単語を使ってしまっていたためエラーがでてたっぽい

悩んで悩みまくって、
「これ予約語じゃね?」と思いmethod から way へとカラム名を変更した結果、無事通った!
よかった〜〜〜〜〜〜〜〜!!!!!!!

正直何かのバグかもとか考えたけど、このモデル以外はCRUD操作ができたからこのモデルで何か間違いがあるんだろうと考えてたけど

はあ、よかった。

予約語使っちゃうみたいな超凡ミスには気をつけてね!