LocomotiveCMS のページにユーザー認証機能を組み込む(Engineの拡張)


ユーザー登録/ログインしないと見れないページを LocomotiveCMS に組み込みたくなりましたので、やってみました。Rails のバージョンは3.2.16、LocomotiveCMS は2.4.1が対象です。

ユーザークラスの作成

まず、LocomotiveCMS には最初から devise というRails用の認証プラグインが組み込まれていますので、新しい認証機能を付け加えるのは簡単そうです。LocomotiveCMS 上ではAccountモデルというのが、CMS自体の管理者ユーザーの管理に使われていますが、それとは別にしたいのでUserモデルを新たに作ってみます。

bundle exec rails g devise User

LocomotiveCMSでは mongoid を使っているので、db:migrate は必要ありません。unicorn_rails の再起動をして、http://localhost:8080/users/sign_up を開いてみると、登録画面が表示されるはずです。

ここでID/Passを登録すると、ユーザ登録がされた状態でトップ画面にリダイレクトされます。mongodb 側を見てみると、確かにユーザーが作られています。

% mongo localhost:27017
MongoDB shell version: 2.4.9
connecting to: localhost:27017/test
Server has startup warnings:
Thu Mar 27 17:45:06.188 [initandlisten]
Thu Mar 27 17:45:06.188 [initandlisten] ** WARNING: soft rlimits too low. Number of files is 256, should be at least 1000
> use mysite_dev
switched to db mysite_dev
> db.users.find()
{ "_id" : ObjectId("533675464857099c8d000001"), "current_sign_in_at" : ISODate("2014-03-29T07:24:54.852Z"), "current_sign_in_ip" : "127.0.0.1", "email" : "[email protected]", "encrypted_password" : "$2a$1*******", "last_sign_in_at" : ISODate("2014-03-29T07:24:54.852Z"), "last_sign_in_ip" : "127.0.0.1", "sign_in_count" : 1 }
>

デザインや追加の登録項目を作るのはとりあえず後回しで先に進みます。

PageController の拡張

LocomotiveCMS はEngineを使っているので、公式ガイドで書かれているように、LocomotiveCMS engine 側のパスと同じ場所にモデルやコントローラーのクラスを置くことで機能を拡張できます。
development.log を見てみると、公開ページへのGETリクエストは以下のコントローラーにたどり着くようです。

development.log
Started GET "/outline" for 127.0.0.1 at 2014-03-30 11:45:19 +0900
Creating scope :root. Overwriting existing method Locomotive::Page.root.
Processing by Locomotive::Public::PagesController#show as HTML
  Parameters: {"path"=>"outline"}

ですので、今回はこのPagesControllerを拡張してみます。まず、LocomotiveCMS Engine側と同じように、app/controllers/locomotive/public/pages_controller.rb に拡張用のクラスを作成します。

bash
cd {RAILS_ROOT}
mkdir -p app/controllers/locomotive/public/
vi app/controllers/locomotive/public/pages_controller.rb

中身は以下のようにしておきます。これで、既存のPagesControllerクラスを再オープンして、新たな定義を追加することができます。Auth::AuthPage クラスはこれから作る認証用のプラグインです。

pages_controller.rb
module Locomotive
  module Public
    class PagesController < ApplicationController
      include Auth::AuthPage #AuthPageというプラグインを読み込む
    end
  end
end

実際に認証機能を追加する、Auth::AuthPage クラスを作ります。元々 /app/concerns 以下は autoload されるようになっているので、そこに以下のクラスを作ります。

app/concerns/auth/auth_page.rb
module Auth
  module AuthPage
    extend ActiveSupport::Concern

    included do
      before_filter :authcheck
    end
    def authcheck
        logger.info "Auth Check"
    end
  end
end

この状態でサイトにアクセスすると、ログファイルに Auth Check というログが出力されているはずです。

logs/development.log
Started GET "/entry" for 127.0.0.1 at 2014-03-30 22:07:38 +0900
Processing by Locomotive::Public::PagesController#show as HTML
  Parameters: {"path"=>"entry"}
[LocomotiveCMS] [fetch site] host = localhost / localhost:8080
  MOPED: 127.0.0.1:27017 QUERY        database=mysite_dev collection=locomotive_sites selector={"$query"=>{}, "$orderby"=>{:_id=>1}} flags=[:slave_ok] limit=-1 skip=0 batch_size=nil fields=nil (0.5510ms)
Auth Check  ←これ
  MOPED: 127.0.0.1:27017 QUERY 

まだログを出力するだけで実際の認証処理は行なっていません。とりあえずはこのままにしておいて、先に認証のオンオフをページ毎に設定できるようにしましょう。

ページ属性に、ログイン必須オプションを追加する

ページに対して、ログインが必須になるような設定を追加できるようにします。app/concerns/auth/auth_field.rb を追加し、以下の内容を記述します。

app/concerns/auth/auth_field.rb
module Auth
  module AuthField
    extend ActiveSupport::Concern
    included do
      field :require_login,         type: Boolean, localize: true
    end
  end
end

上記モジュールをPageモデルからincludeすることで、require_login というフィールドを作ることができます。
app/models/locomotive/page.rb ファイルを新たに作り、include 文を記載します。

app/models/locomotive/page.rb
module Locomotive
  class Page
    include Auth::AuthField
  end
end

Page クラスが正しく拡張されているか、確かめてみましょう。rails console からLocomotie.Pageクラスを確認してみます。

% bundle exec rails console
Loading development environment (Rails 3.2.16)
[1] pry(main)> 
[2] pry(main)> Locomotive::Page.first()
=> #<Locomotive::Page _id: 53303a0c485709d69800000e, created_at: 2014-03-24 13:58:36 UTC, updated_at: 2014-03-27 03:27:16 UTC, parent_id: "5330384f485709d698000004", parent_ids: ["5330384f485709d698000004"], position: 0, depth: 1, serialized_template: {"ja"=>#<Moped::BSON::Binary type=:generic length=7286>}, template_dependencies: {"ja"=>["5330384f485709d698000004"]}, snippet_dependencies: {"ja"=>["footer"]}, templatized: false, templatized_from_parent: false, target_klass_name: nil, redirect: false, redirect_url: nil, redirect_type: 301, listed: true, seo_title: {"ja"=>"お知らせ"}, meta_keywords: {"ja"=>"html5, "}, meta_description: {"ja"=>"html5, "}, title: {"ja"=>"お知らせ"}, slug: {"ja"=>"news"}, fullpath: {"ja"=>"news"}, handle: "news", raw_template: {"ja"=>"{% extends 'parent' %}\n{% block main %}\n<div class='row'>\n  <div class='large-8 columns'>\n    <p>News</p>\n    {% editable_text \"news_body\", rows: 20 %}\n    hoge\n    {% endeditable_text %}\n  </div>\n</div>\n{% endblock %}\n", "en"=>"{% extends 'parent' %}"}, locales: [:ja], published: true, cache_strategy: "simple", response_type: "text/html", site_id: "5330384f485709d698000002", require_login: nil, cache_strategy_text: "Simple", lang: "ja">

require_loginというプロパティがあるのを確認できました。

管理画面から切り替えができるようにする

さて、あと一息です。今度は管理画面のViewにrequire login属性を追加しましょう。
管理画面のページは、/locomotive/pages/{ID}/edit というURLになっていますので、Engine 側の /app/views/locomotive/pages/edit.html.hamlを見てみますと、ページの下の方に以下のような記述があります。

/app/views/locomotive/pages/edit.html.haml
= semantic_form_for @page, url: page_path(@page), html: { multipart: true } do |form|

   = render 'form', f: form ←これ

   = render 'locomotive/shared/form_actions', back_url: pages_path, button_label: :update

というわけで、今度は /app/views/locomotive/pages/_form.html.haml を開きますと、それっぽいファイルが出てきました。

/app/views/locomotive/pages/_form.html.haml
- content_for :head do                                                                                     
  = render '/locomotive/content_assets/picker'
  = render '/locomotive/theme_assets/picker'
  = render 'editable_elements'

- content_for :backbone_view_data do
  :plain
    page: #{escape_json to_json(@page)}

= f.inputs name: :information do

  = f.input :title, wrapper_html: { class: 'highlighted' }
(省略)

まさに求めていたファイルそのものです。このフォームに一行追加をしたいので、このファイルを自分のアプリケーションの下の同じパスにコピーして開きます。

cp {オリジナルのEngineをcloneしておいた場所}/app/views/locomotive/pages/_form.html.haml {RAILS_ROOT}/app/views/locomotive/pages/_form.html.haml
vi {RAILS_ROOT}/app/views/locomotive/pages/_form.html.haml

その後そのファイルを開き、published フィールドの行の下に以下のように行を追加します。

/app/views/locomotive/pages/_form.html.haml
  = f.input :published, as: :'Locomotive::Toggle', input_html: { class: 'simple-toggle' }

  = f.input :require_login, as: :'Locomotive::Toggle', input_html: { class: 'simple-toggle' } #この行を追加

  = f.input :listed, as: :'Locomotive::Toggle', input_html: { class: 'simple-toggle' }

(この一行追加するためだけに元のviewファイル全体をコピーしてくるのは、今後の本家のバージョンアップへの追随などを考えると少々面倒。全部コピーせずに実現する方法があれば教えて欲しいです!)

そして管理画面からページ編集画面を見てみます。

無事新しいフィールドが編集できるようになりました!ONにするとちゃんと新しい属性が更新されるようになっているはずです。

認証が必要なページでログイン画面を出力する

いよいよ最後のステップです。PageController の拡張の時に作った AuthPage モジュールを以下のように書き換えます。

app/concerns/auth/auth_page.rb
module Auth
  module AuthPage
    extend ActiveSupport::Concern

    included do
      before_filter :authcheck
    end
    def authcheck
      @page ||= self.locomotive_page
      authenticate_user! if @page && @page.require_login                                                            
    end
  end
end

locomotive_page というメソッドは元々のコントローラーが持っていたメソッドで、表示対象のページを取得するものです。authenticate_user! メソッドは、devise が提供するもので、ログインチェックをしてログインしていなければログイン画面へリダイレクト、ログイン後戻ってくるというすぐれものです。
ページのrequire_login属性がtrueの場合のみ、このauthenticate_user! メソッドを叩くという処理をしているわけです。

この状態で、先ほど requre_login を ON にしたページにログインをすると、以下のようにログイン画面が出てくるはずです。

ログインを行うと、元々表示したかったページが表示されます。一方、require_loginを設定していないページは、今まで通りログインしなくても表示できます。

以上です!

(追記)ここまで書いたものはローカルの開発環境では動いているのですが、なぜか上記手順で作ったものを Heroku にデプロイすると、AuthFieldクラスがundefined method 'field'というエラーを吐いて落ちてしまうという事象が発生中。どうもapp/models/locomotive/page.rb やapp/controllers/locomotive/public/pages_controller.rb を置いた時点で、Engine側のクラスが読み込まれず新しいクラスで上書き状態になってしまっている模様。解決策が分かる人がいたら教えて下さい!
2014/04/07追記:とりあえず現状では、engine 側のファイルをコピって来て、それにメソッドを追加したものをデプロイして動いています。ダサい・・・


Locomotive 関連の記事一覧:
* Locomotive Engine を Heroku で動かすまで
* LocomotiveCMS でニュース記事一覧機能を作る
* LocomotiveCMS のページにユーザー認証機能を組み込む(Engineの拡張)
* LocomotiveCMS + Devise を使って作ったログインページのデザインをCMS側でできるようにする(1)
* LocomotiveCMS + Devise を使って作ったログインページのデザインをCMS側でできるようにする(2)
* LocomotiveCMS + Devise を使って作ったログインページのデザインをCMS側でできるようにする(3)
* LocomotiveCMS で、複数の Heroku インスタンスを使う