【deviseに疲れた人のための】sorceryでユーザログイン機能を実装

deviseの代替案として、前々からsoceryいいよなんてきいてたので使ってみます。

github.com

@komagata さんにおききしたらつらみはそんなに減らないとなったのが一瞬頭によぎる

まぁ…使ってみないことには分からないのでやってみましょう。monbanも気になるところですが。

Special Thanks

  • NoamB/sorcery

公式が一番。

sorcery/README.md at master · NoamB/sorcery · GitHub

  • workabroad

コードめちゃくちゃ参考にさせていただきました。

www.workabroad.jp

  • classmethod

dev.classmethod.jp

  • RailsCasts

わかりやすいのですがRailsRubyがかなり古いので注意。

#283 Authentication with Sorcery - RailsCasts

こんな感じ

最終形態はこんな感じに、

  • ユーザ登録
  • ログイン
  • ログアウト

できる感じになります。

movie.gif

初期設定

環境

今回実装している環境はこんな感じ。

$ cat .ruby-version
2.3.0
$ be rails -v
Rails 5.0.0.beta3

インストール

Gemfileにsorceryを追加

# Gemfile
gem "sorcery"

インストールします。ひとまずsubmoduleはremember_meだけで。できるだけミニマムに保ちたいので必要になってから追加していくようにします。

$ rails generate sorcery:install remember_me

      create  config/initializers/sorcery.rb
        gsub  config/initializers/sorcery.rb
    generate  model User --skip-migration
Running via Spring preloader in process 31534
      invoke  active_record
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb
      invoke      factory_girl
      create        spec/factories/users.rb
      insert  app/models/user.rb
      insert  app/models/user.rb
      create  db/migrate/20160321031406_sorcery_core.rb
      create  db/migrate/20160321031407_sorcery_remember_me.rb

追加の場合は以下のようにすればokです。

$ rails generate sorcery:install remember_me --only-submodules

ログインさせたいモデルのクラス名がUserでない場合はconfigファイルで変更が必要です。この部分。

  config.user_class = "User"

username_attributeの変更

また、今回はemailだけでなくusername(Twitterでいうid的なイメージ)でもログインできるようにしたいと思います。

まずは以下ようにarrayにusernameも入れてあげます。

Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    # -- core --
    # specify username attributes, for example: [:username, :email].
    # Default: `[:email]`
    #
    user.username_attribute_names = %i(email username) # ここ修正
  end
end

そしたら、最初に生成されたSorceryCoreのmigrationファイルもいじっておきます。

class SorceryCore < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :username, null: false # ここ追加
      t.string :email, null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps
    end

    add_index :users, :email, unique: true
  end
end

Model

Userのモデルファイルも確認だけしてみます。空のクラスファイルにauthenticates_with_sorcery!というメソッドが追加されてました。これでUserモデルで必要なクラスメソッドとインスタンスメソッドが得られるというわけですね。

ということで必要に応じてvalidationなど設定しときます。

# app/models/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :username,
    presence: true,
    uniqueness: true
  validates :email,
    presence: true,
    uniqueness: true
  validates :password,
    presence: true,
    length: { minimum: 6 }, if: -> { new_record? || changes["password"] }
end
# spec/models/user.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe "validations" do
    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_uniqueness_of(:email) }
    it { is_expected.to validate_presence_of(:username) }
    it { is_expected.to validate_uniqueness_of(:username) }
    it { is_expected.to validate_presence_of(:password) }
  end
end

Routing

ルーティングの設定をしておきます。こんな感じで。

# config/routes.rb
Rails.application.routes.draw do
  root "users#new"

  resources :users, only: %i(new create show)
  resources :sessions, only: %i(new create destroy)
end

Controller

次はcontroller部分を作っていきます。

$ bin/rails g controller users new
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def show
    @user = User.find(params[:id])
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to user_path(@user), notice: "Signed up!"
    else
      render :new, notice: "Failed to signin"
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :email, :password)
  end
end

セッションを管理するcontrollerをnewアクションをつけて生成しておきます。

$ bin/rails g controller sessions new
# app/controllers/sessions_controller.rb
  def new
  end

  def create
    user = login(params[:email], params[:password], params[:remember_me])
    if user
      redirect_back_or_to user_path(user), notice: "Logged in!"
    else
      flash.now[:alert] = "Failed to login"
      render :new
    end
  end

  def destroy
    logout
    redirect_to root_path, notice: "Logged out!"
  end

View

続いてViewを作っていきます。

k0kubunさんにお世話になります。hamlitを使います。

# Gemfile
gem "hamlit"

Viewファイルをそれぞれ作成します。

  • users/new.html.haml
%h2 Registering New User

= form_for @user do |f|
  = render 'shared/error_messages', object: f.object

  .field
    = f.label :username
    = f.text_field :username
  .field
    = f.label :email
    = f.text_field :email
  .field
    = f.label :password
    = f.password_field :password
  .actions
    = f.submit

%p
  or
  = link_to "login", new_session_path
  • users/show.html.haml
%h2 User MyPage

= "username: #{@user.username}"
%br
= "email: #{@user.email}"
%br
= link_to "log out", session_path, method: :DELETE
  • sessions/new.html.haml
%h2 User Login

= render 'shared/flash_message'

- if logged_in?
  = current_user.username
  = current_user.email

= form_tag sessions_path do
  .field
    = label_tag :username
    = text_field_tag :username, params[:username]
  .field
    = label_tag :email
    = text_field_tag :email, params[:email]
  .field
    = label_tag :password
    = password_field_tag :password
  .field
    = check_box_tag :remember_me, 1, params[:remember_me]
    = label_tag :remember_me
  .actions
    = submit_tag "Log in"

%p
  or
  = link_to "Signup", new_user_path

おわり

ということでこんな感じで完成!

movie.gif

sorceryを使った感じでは「ユーザログインに必要なメソッドを一式提供してくれますよ」くらいなgemの印象。

deviseは「ユーザログインに必要なこと全部やります」って感じにcontrollerのアクションのロジックまで全て用意するのに対してsorceryはビジネスロジックだけ提供してくれる、みたいな。

もう少しsorcery使ってみます。今のところ好印象。