Devise: вход и регистрация в модальных окнах

от автора

На проекте необходимо было сделать логин через модальные окна и «обычные» страницы для разных типов устройств. После поиска понял, что зачастую описывается не совсем то, что нужно. Так здесь просто помещают форму в модальное окно (фактически пользуясь страницей из wiki devise), а тут (вход и регистрация) переопределяют методы в контроллерах devise так, что они постоянно отдают только json и для «немодального» поведения нужно будет писать много условий с проверкой формата запроса. Поэтому я решил поэкспериментировать в новом приложении и написать поддержку 2 форматов с минимальным количеством переопределения и грязных хаков.

Создание приложения

  1. Генерим приложение без тестов и запуска bundle install: rails new devise_modal -B -T
  2. Добавляем нужные гемы в Gemfile:
    • gem 'therubyracer', platforms: :ruby
      gem "less-rails"
      gem 'twitter-bootstrap-rails', branch: 'bootstrap3' — для модальных окон используем bootstrap
    • gem 'devise' авторизация будет через devise

    И устанавливаем всё: bundle install

  3. Запускаем нужные генераторы
    rails g bootstrap:install static, «static» так как ничего менять в стилях bootstrap‘а не будем
    rails g devise:install; rails g devise User; rake db:migrate — устанавливаем devise и создаём пользователя
  4. Создаём контроллер, который будет отображать главную страницу:
    rails g controller welcome index --no-helper --no-assets
    В config/routes.rb привязываем index к главной странице:
    root 'welcome#index'

В конце этого этапа есть приложение, с формами входа/регистрации на стандартных ссылках для devise: users/sign_in и users/sign_up.

Модальные окна для форм

В формах нету ничего примечательного — используем стандартные devise‘овские сделав их remote и поменяв формат на json. Дальше делаем их модальными, обернув в соответствующие классы bootstrap‘а. В итоге получились такие partial‘ы:
app/views/shared/_sign_in.html.erb

<div class="modal hide fade in" id="sign_in">      <div class="modal-header">     <button class="close" data-dismiss="modal">x</button>     <h2>Sign in</h2>   </div>    <div class="modal-body">     <%= form_for(resource, as: resource_name, url: session_path(resource_name), html:{id: 'sign_in_user', :'data-type' => 'json'}, remote: true) do |f| %>       <div>         <%= f.label :email %><br />         <%= f.email_field :email, autofocus: true %>       </div>        <div>         <%= f.label :password %><br />         <%= f.password_field :password, autocomplete: "off" %>       </div>        <% if devise_mapping.rememberable? -%>         <div>           <%= f.check_box :remember_me %>           <%= f.label :remember_me %>         </div>       <% end -%>        <div>         <%= f.submit "Sign in" %>       </div>     <% end %>   </div>   <div class="modal-footer">   </div> </div> 

app/views/shared/_sign_up.html.erb

<div class="modal hide fade in" id="sign_up">      <div class="modal-header">     <button class="close" data-dismiss="modal">x</button>     <h2>Sign up</h2>   </div>    <div class="modal-body">     <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: {id: 'sign_up_user', :'data-type' => 'json'}, remote: true) do |f| %>       <%= devise_error_messages! %>        <div>         <%= f.label :email %><br />         <%= f.email_field :email, autofocus: true %>       </div>        <div>         <%= f.label :password %><br />         <%= f.password_field :password, autocomplete: "off" %>       </div>        <div>         <%= f.label :password_confirmation %><br />         <%= f.password_field :password_confirmation, autocomplete: "off" %>       </div>        <div><%= f.submit "Sign up" %></div>     <% end %>   </div>   <div class="modal-footer">   </div> </div> 

Добавим отображение этих файлов и ссылок для их вызова в layout:

<%= link_to "Sign in", "#sign_in", "data-toggle" => "modal", :class => 'btn btn-small' %> <%= link_to "Sign up", "#sign_up", "data-toggle" => "modal", :class => 'btn btn-small' %> <%= render 'shared/sign_in' %> <%= render 'shared/sign_up' %> 

А после этого облагородим немного, сделав проверку на наличие юзера:
app/views/layouts/application.html.erb

<% if current_user %>   <%= "Hello, #{current_user.email}" %>   <%= link_to "Sign out", destroy_user_session_path, :method => :delete %> <% else %>   <%= link_to "Sign in", "#sign_in", "data-toggle" => "modal", :class => 'btn btn-small' %>   <%= link_to "Sign up", "#sign_up", "data-toggle" => "modal", :class => 'btn btn-small' %>   <%= render 'shared/sign_in' %>   <%= render 'shared/sign_up' %> <% end %> 

Чтобы всё это работало нужно добавить несколько методов в application_helper, которые определяют resource и связанные с ним вещи для данного контекста:
app/helpers/application_helper.rb

def resource_name   :user end  def resource   @resource ||= User.new end  def devise_mapping   @devise_mapping ||= Devise.mappings[:user] end 

Теперь есть модальные формы, которые доступны с любой страницы и позволяют входить и регистрироваться. Осталось только сделать, чтобы devise на эти запросы в ответ не отправлял html страницы.

JSON ответы от devise

В геме devise за ошибки связанные со входом отвечает FailureApp. При возникновении ошибки в SessionsController‘е, который отрабатывает запросы на вход, вызывается respond, где с помощью http_auth? проверяется: нужно слать 401 статус или же переадресовывать на другую страницу. Так как по умолчанию у devise‘а:
config/initializers/devise.rb

config.http_authenticatable_on_xhr = true 

то и возвращается 401.
RegistrationsController же в ответ на AJAX запрос присылает html страницу, чтобы это исправить переопределим его немного — укажем явно, какие форматы нас интересуют:
rails g controller Registrations --no-helper --no-assets --no-views
config/routes.rb

devise_for :users, controllers: {registrations: 'registrations'} 

app/controllers/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController   respond_to :html, :json end 

Теперь при неудачной попытке регистрации будет отдаваться 422 статус с текстами ошибок в responseJSON[‘errors’], а при удачной — 201. Аналогично для SessionsController‘а при удачном входе нужно отдавать статус, а не html-страницу, поэтому «научим» и его правильно реагировать на json запросы:
rails g controller Sessions --no-helper --no-assets --no-views
config/routes.rb

devise_for :users, controllers: {sessions: 'sessions', registrations: 'registrations'} 

app/controllers/sessions_controller.rb

class SessionsController < Devise::SessionsController   respond_to :html, :json end 

Также можно написать javascript, который будет обрабатывать ответы от модальных форм, например такой:
app/assets/javascripts/welcome.js.coffee

$ ->   $("form#sign_in_user, form#sign_up_user").bind("ajax:success", (event, xhr, settings) ->     $(this).parents('.modal').modal('hide')   ).bind("ajax:error", (event, xhr, settings, exceptions) ->     error_messages = if xhr.responseJSON['error']       "<div class='alert alert-danger pull-left'>" + xhr.responseJSON['error'] + "</div>"     else if xhr.responseJSON['errors']       $.map(xhr.responseJSON["errors"], (v, k) ->         "<div class='alert alert-danger pull-left'>" + k + " " + v + "</div>"       ).join ""     else       "<div class='alert alert-danger pull-left'>Unknown error</div>"     $(this).parents('.modal').children('.modal-footer').html(error_messages)   ) 

При входе оборачиваем ошибку в alert, а при регистрации — ошибки по каждому параметру, после чего выводим полученное сообщение в footer‘е. При успешном запросе просто убираем модальную форму (можно ещё обновлять блок в layout‘е, в котором проверяется наличие пользователя, чтобы отображать данные пользователя (они также приходят в ответе)).
Теперь контроллеры отдают ответы в правильном формате, как и для модальных форм — json, так и для стандартных(users/sign_in, users/sign_up) — html. И всё, что понадобилось для этого понадобилось — переопределить контроллеры, расширив набор форматов:

respond_to :html, :json 

Примечание

Приложение писалось на rails 4, но отличия для 3.2 будут минимальны: запустится bundle install при создании приложения, нужно будет удалить public/index.html а также путь на главную будет выглядеть чуть иначе:
config/routes.rb

root to: 'welcome#index' 

ссылка на оригинал статьи http://habrahabr.ru/post/216837/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *