From 158e20393692bd2726794c83a95206a3cf7f02a3 Mon Sep 17 00:00:00 2001 From: David Gil Date: Mon, 24 Aug 2015 19:44:46 +0200 Subject: [PATCH] adds omniauth basic authentication process with Twitter, including an intermediate step to ask the user for her email if not provided by the OAuth provider - Twitter, for instance --- app/controllers/application_controller.rb | 11 ++++ .../users/omniauth_callbacks_controller.rb | 30 ++++++++++ .../users/registrations_controller.rb | 15 +++++ app/models/user.rb | 55 ++++++++++++++++++- app/views/devise/_omniauth_form.html.erb | 5 ++ app/views/devise/menu/_login_items.html.erb | 14 ++--- app/views/devise/passwords/new.html.erb | 2 +- app/views/devise/sessions/new.html.erb | 4 +- .../registrations/finish_signup.html.erb | 13 +++++ app/views/users/registrations/new.html.erb | 3 + config/initializers/devise.rb | 1 + config/initializers/omniauth.rb | 4 -- config/locales/devise.es.yml | 2 +- config/locales/en.yml | 6 ++ config/locales/es.yml | 6 ++ config/routes.rb | 15 ++++- 16 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 app/controllers/users/omniauth_callbacks_controller.rb create mode 100644 app/views/devise/_omniauth_form.html.erb create mode 100644 app/views/users/registrations/finish_signup.html.erb delete mode 100644 config/initializers/omniauth.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b879a4a8d..802dda3bc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,6 +13,8 @@ class ApplicationController < ActionController::Base # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception + before_action :ensure_signup_complete + rescue_from CanCan::AccessDenied do |exception| redirect_to main_app.root_url, alert: exception.message end @@ -40,4 +42,13 @@ class ApplicationController < ActionController::Base def set_debate_votes(debates) @voted_values = current_user ? current_user.debate_votes(debates) : {} end + + def ensure_signup_complete + # Ensure we don't go into an infinite loop + return if action_name.in? %w(finish_signup do_finish_signup) + + if user_signed_in? && !current_user.email_provided? + redirect_to finish_signup_path + end + end end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..aac6583d1 --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,30 @@ +class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + def self.provides_callback_for(provider) + class_eval %Q{ + def #{provider} + @user = User.find_for_oauth(env["omniauth.auth"], current_user) + + if @user.persisted? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format? + else + session["devise.#{provider}_data"] = env["omniauth.auth"] + redirect_to new_user_registration_url + end + end + } + end + + # [:twitter, :facebook, :google_oauth2].each do |provider| + [:twitter].each do |provider| + provides_callback_for provider + end + + def after_sign_in_path_for(resource) + if resource.email_provided? + super(resource) + else + finish_signup_path + end + end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 0ef0ba638..d1fc54aab 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,4 +1,5 @@ class Users::RegistrationsController < Devise::RegistrationsController + prepend_before_filter :authenticate_scope!, only: [:edit, :update, :destroy, :finish_signup, :do_finish_signup] def create build_resource(sign_up_params) @@ -9,6 +10,20 @@ class Users::RegistrationsController < Devise::RegistrationsController end end + def finish_signup + end + + def do_finish_signup + if current_user.update(sign_up_params) + current_user.skip_reconfirmation! + sign_in(current_user, bypass: true) + redirect_to root_url, notice: I18n.t('devise.registrations.updated') + else + @show_errors = true + render :finish_signup + end + end + private def sign_up_params diff --git a/app/models/user.rb b/app/models/user.rb index 544142c2f..4a1839b34 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,8 +1,12 @@ class User < ActiveRecord::Base include ActsAsParanoidAliases + + OMNIAUTH_EMAIL_PREFIX = 'omniauth@participacion' + OMNIAUTH_EMAIL_REGEX = /\A#{OMNIAUTH_EMAIL_PREFIX}/ + apply_simple_captcha devise :database_authenticatable, :registerable, :confirmable, - :recoverable, :rememberable, :trackable, :validatable + :recoverable, :rememberable, :trackable, :validatable, :omniauthable acts_as_voter acts_as_paranoid column: :hidden_at @@ -11,9 +15,11 @@ class User < ActiveRecord::Base has_one :moderator has_one :organization has_many :inappropiate_flags + has_many :identities, dependent: :destroy - validates :username, presence: true, unless: :organization? + validates :username, presence: true, unless: :organization? validates :official_level, inclusion: {in: 0..5} + validates_format_of :email, without: OMNIAUTH_EMAIL_REGEX, on: :update validates_associated :organization, message: false @@ -25,6 +31,47 @@ class User < ActiveRecord::Base scope :organizations, -> { joins(:organization) } scope :officials, -> { where("official_level > 0") } + def self.find_for_oauth(auth, signed_in_resource = nil) + # Get the identity and user if they exist + identity = Identity.find_for_oauth(auth) + + # If a signed_in_resource is provided it always overrides the existing user + # to prevent the identity being locked with accidentally created accounts. + # Note that this may leave zombie accounts (with no associated identity) which + # can be cleaned up at a later date. + user = signed_in_resource ? signed_in_resource : identity.user + + # Create the user if needed + if user.nil? + + # Get the existing user by email if the provider gives us a verified email. + # If no verified email was provided we assign a temporary email and ask the + # user to verify it on the next step via RegistrationsController.finish_signup + email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email) + email = auth.info.email if email_is_verified + user = User.where(email: email).first if email + + # Create the user if it's a new registration + if user.nil? + user = User.new( + username: auth.info.nickname || auth.extra.raw_info.name.parameterize('-') || auth.uid, + email: email ? email : "#{OMNIAUTH_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", + password: Devise.friendly_token[0,20] + ) + user.skip_confirmation! + user.save! + end + end + + # Associate the identity with the user if needed + if identity.user != user + identity.user = user + identity.save! + end + + user + end + def name organization? ? organization.name : username end @@ -67,4 +114,8 @@ class User < ActiveRecord::Base e.present? ? where(email: e) : none end + def email_provided? + !!(email && email !~ OMNIAUTH_EMAIL_REGEX) || + !!(unconfirmed_email && unconfirmed_email !~ OMNIAUTH_EMAIL_REGEX) + end end diff --git a/app/views/devise/_omniauth_form.html.erb b/app/views/devise/_omniauth_form.html.erb new file mode 100644 index 000000000..888a3ba2f --- /dev/null +++ b/app/views/devise/_omniauth_form.html.erb @@ -0,0 +1,5 @@ +
+ +<%= link_to t("omniauth.twitter.sign_in"), user_omniauth_authorize_path(:twitter), class: 'button radius expand' %> + +
diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index 4959553f9..6cd27160d 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -7,11 +7,11 @@ <%= link_to(t("devise_views.menu.login_items.logout"), destroy_user_session_path, method: :delete) %> <% else %> -
  • - <%= link_to(t("devise_views.menu.login_items.login"), new_user_session_path) %> -
  • -
  • - <%= link_to(t("devise_views.menu.login_items.signup"), new_user_registration_path, class: "button radius small") %> -
  • +
  • + <%= link_to(t("devise_views.menu.login_items.login"), new_user_session_path) %> +
  • +
  • + <%= link_to(t("devise_views.menu.login_items.signup"), new_user_registration_path, class: "button radius small") %> +
  • <% end %> - \ No newline at end of file + diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb index 78a8b8b4c..1219c393d 100644 --- a/app/views/devise/passwords/new.html.erb +++ b/app/views/devise/passwords/new.html.erb @@ -19,4 +19,4 @@ <%= render "devise/shared/links" %> - \ No newline at end of file + diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 391ef186c..ab0b22426 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -3,6 +3,8 @@

    <%= t("devise_views.sessions.new.title") %>

    + <%= render 'devise/omniauth_form' %> + <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
    @@ -28,4 +30,4 @@ <%= render "devise/shared/links" %>
    -
    \ No newline at end of file + diff --git a/app/views/users/registrations/finish_signup.html.erb b/app/views/users/registrations/finish_signup.html.erb new file mode 100644 index 000000000..c57f01a8e --- /dev/null +++ b/app/views/users/registrations/finish_signup.html.erb @@ -0,0 +1,13 @@ +
    +
    +
    +

    <%= t('omniauth.finish_signup.title') %>

    + + <%= form_for current_user, as: :user, url: do_finish_signup_path, html: { role: 'form'} do |f| %> + <%= render 'shared/errors', resource: current_user %> + <%= f.email_field :email, placeholder: t("devise_views.users.registrations.new.email_label"), value: nil %> + <%= f.submit t("devise_views.users.registrations.new.submit"), class: 'button radius' %> + <% end %> +
    +
    +
    diff --git a/app/views/users/registrations/new.html.erb b/app/views/users/registrations/new.html.erb index 92344b76f..2ac5b7af6 100644 --- a/app/views/users/registrations/new.html.erb +++ b/app/views/users/registrations/new.html.erb @@ -2,6 +2,9 @@

    <%= t("devise_views.users.registrations.new.title") %>

    + + <%= render 'devise/omniauth_form' %> + <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render 'shared/errors', resource: resource %> diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f7cdfb292..06c4740ba 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -239,6 +239,7 @@ Devise.setup do |config| # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + config.omniauth :twitter, Rails.application.secrets.twitter_key, Rails.application.secrets.twitter_secret # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb deleted file mode 100644 index 42e97b02f..000000000 --- a/config/initializers/omniauth.rb +++ /dev/null @@ -1,4 +0,0 @@ -Rails.application.config.middleware.use OmniAuth::Builder do - provider :developer unless Rails.env.production? - provider :twitter, Rails.application.secrets.twitter_key, Rails.application.secrets.twitter_secret -end diff --git a/config/locales/devise.es.yml b/config/locales/devise.es.yml index f39099f57..c2bc246e9 100644 --- a/config/locales/devise.es.yml +++ b/config/locales/devise.es.yml @@ -55,4 +55,4 @@ es: not_locked: "no estaba bloqueado." not_saved: one: "1 error impidió que este %{resource} fuera guardado:" - other: "%{count} errores impidieron que este %{resource} fuera guardado:" \ No newline at end of file + other: "%{count} errores impidieron que este %{resource} fuera guardado:" diff --git a/config/locales/en.yml b/config/locales/en.yml index ce3339b25..725955c30 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -148,3 +148,9 @@ en: all: "You are not authorized to %{action} %{subject}." welcome: last_debates: Last debates + omniauth: + finish_signup: + title: Add Email + twitter: + sign_in: Sign in with Twitter + or: or diff --git a/config/locales/es.yml b/config/locales/es.yml index 1e2cca785..efd0fd88e 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -148,3 +148,9 @@ es: all: "No tienes permiso para realizar la acción '%{action}' sobre %{subject}." welcome: last_debates: Últimos debates + omniauth: + finish_signup: + title: Añade tu email + twitter: + sign_in: Inicia sessión con Twitter + or: o diff --git a/config/routes.rb b/config/routes.rb index f224609b9..a51c75c39 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,19 @@ Rails.application.routes.draw do - devise_for :users, controllers: { registrations: 'users/registrations' } + devise_for :users, controllers: { + registrations: 'users/registrations', + omniauth_callbacks: 'users/omniauth_callbacks' + } devise_for :organizations, class_name: 'User', controllers: { registrations: 'organizations/registrations', - sessions: 'devise/sessions' - } + sessions: 'devise/sessions', + }, + skip: [:omniauth_callbacks] + + devise_scope :user do + get :finish_signup, to: 'users/registrations#finish_signup' + patch :do_finish_signup, to: 'users/registrations#do_finish_signup' + end # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes".