diff --git a/Gemfile b/Gemfile index f1b251db4..14029a3c9 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.3' +gem 'rails', '4.2.4' # Use PostgreSQL gem 'pg' # Use SCSS for stylesheets @@ -21,6 +21,11 @@ gem 'turbolinks' gem 'devise' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' +gem 'omniauth' +gem 'omniauth-twitter' +gem 'omniauth-facebook' +gem 'omniauth-google-oauth2' + gem 'kaminari' gem 'acts_as_commentable_with_threading' gem 'acts-as-taggable-on' @@ -32,7 +37,7 @@ gem 'simple_captcha2', require: 'simple_captcha' gem 'ckeditor' gem 'cancancan' gem 'social-share-button' -gem 'initialjs-rails' +gem 'initialjs-rails', '0.2.0' gem 'unicorn' gem 'paranoia' diff --git a/Gemfile.lock b/Gemfile.lock index deed6bc4f..adeb5d99f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,36 +1,36 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.3) - actionpack (= 4.2.3) - actionview (= 4.2.3) - activejob (= 4.2.3) + actionmailer (4.2.4) + actionpack (= 4.2.4) + actionview (= 4.2.4) + activejob (= 4.2.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.3) - actionview (= 4.2.3) - activesupport (= 4.2.3) + actionpack (4.2.4) + actionview (= 4.2.4) + activesupport (= 4.2.4) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.3) - activesupport (= 4.2.3) + actionview (4.2.4) + activesupport (= 4.2.4) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.3) - activesupport (= 4.2.3) + activejob (4.2.4) + activesupport (= 4.2.4) globalid (>= 0.3.0) - activemodel (4.2.3) - activesupport (= 4.2.3) + activemodel (4.2.4) + activesupport (= 4.2.4) builder (~> 3.1) - activerecord (4.2.3) - activemodel (= 4.2.3) - activesupport (= 4.2.3) + activerecord (4.2.4) + activemodel (= 4.2.4) + activesupport (= 4.2.4) arel (~> 6.0) - activesupport (4.2.3) + activesupport (4.2.4) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -133,6 +133,8 @@ GEM factory_girl_rails (4.5.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) foundation-rails (5.5.2.1) railties (>= 3.1.0) sass (>= 3.3.0, < 3.5) @@ -150,6 +152,7 @@ GEM activesupport (>= 4.1.0) groupdate (2.4.0) activesupport (>= 3) + hashie (3.4.2) highline (1.7.3) http-cookie (1.0.2) domain_name (~> 0.5) @@ -162,13 +165,14 @@ GEM i18n term-ansicolor (>= 1.3.2) terminal-table (>= 1.5.1) - initialjs-rails (0.1.0) + initialjs-rails (0.2.0) railties (>= 3.1, < 5.0) jquery-rails (4.0.4) rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.3) + jwt (1.5.1) kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -181,7 +185,7 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) - loofah (2.0.2) + loofah (2.0.3) nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) @@ -189,12 +193,38 @@ GEM mini_portile (0.6.2) minitest (5.8.0) multi_json (1.11.2) + multi_xml (0.5.5) + multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (2.9.2) netrc (0.10.3) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + oauth (0.4.7) + oauth2 (1.0.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (~> 1.2) + omniauth (1.2.2) + hashie (>= 1.2, < 4) + rack (~> 1.0) + omniauth-facebook (2.0.1) + omniauth-oauth2 (~> 1.2) + omniauth-google-oauth2 (0.2.6) + omniauth (> 1.0) + omniauth-oauth2 (~> 1.1) + omniauth-oauth (1.1.0) + oauth + omniauth (~> 1.0) + omniauth-oauth2 (1.3.1) + oauth2 (~> 1.0) + omniauth (~> 1.2) + omniauth-twitter (1.2.1) + json (~> 1.3) + omniauth-oauth (~> 1.1) orm_adapter (0.5.0) paranoia (2.1.3) activerecord (~> 4.0) @@ -209,28 +239,28 @@ GEM rack (1.6.4) rack-test (0.6.3) rack (>= 1.0) - rails (4.2.3) - actionmailer (= 4.2.3) - actionpack (= 4.2.3) - actionview (= 4.2.3) - activejob (= 4.2.3) - activemodel (= 4.2.3) - activerecord (= 4.2.3) - activesupport (= 4.2.3) + rails (4.2.4) + actionmailer (= 4.2.4) + actionpack (= 4.2.4) + actionview (= 4.2.4) + activejob (= 4.2.4) + activemodel (= 4.2.4) + activerecord (= 4.2.4) + activesupport (= 4.2.4) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.3) + railties (= 4.2.4) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.6) + rails-dom-testing (1.0.7) activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) - railties (4.2.3) - actionpack (= 4.2.3) - activesupport (= 4.2.3) + railties (4.2.4) + actionpack (= 4.2.4) + activesupport (= 4.2.4) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) raindrops (0.15.0) @@ -283,7 +313,7 @@ GEM coffee-rails sass-rails spring (1.3.6) - sprockets (3.2.0) + sprockets (3.3.3) rack (~> 1.0) sprockets-rails (2.3.2) actionpack (>= 3.0) @@ -357,16 +387,20 @@ DEPENDENCIES fuubar groupdate i18n-tasks - initialjs-rails + initialjs-rails (= 0.2.0) jquery-rails kaminari launchy letter_opener_web (~> 1.3.0) + omniauth + omniauth-facebook + omniauth-google-oauth2 + omniauth-twitter paranoia pg poltergeist quiet_assets - rails (= 4.2.3) + rails (= 4.2.4) responders rspec-rails (~> 3.0) sass-rails (~> 5.0) diff --git a/README.md b/README.md index 6c8bbb453..87e0d1ab9 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Para ejecutar la aplicación en local: bundle exec bin/rails s ``` -Prerequisitos para los tests: tener instalado PhantomJS >= 2.0 +Prerequisitos para los tests: tener instalado PhantomJS >= 2.0 Para ejecutar los tests: @@ -44,6 +44,12 @@ Para ejecutar los tests: bundle exec bin/rspec ``` +### OAuth + +Para probar los servicios de autenticación mediante proveedores externos OAuth — en este momento Twitter, Facebook y Google —, necesitas crear una "aplicación" en cada una de las plataformas soportadas y configurar la *key* y el *secret* proporcionados en tu *secrets.yml* + +En el caso de Google, comprueba que las APIs *Contacts API* y *Google+ API* están habilitadas para la aplicación. + ## Licencia El código de este proyecto está publicado bajo la licencia AFFERO GPL v3 (ver [LICENSE-AGPLv3.txt](LICENSE-AGPLv3.txt)) diff --git a/app/assets/stylesheets/participacion.scss b/app/assets/stylesheets/participacion.scss index 619f25d48..e20a2cb36 100644 --- a/app/assets/stylesheets/participacion.scss +++ b/app/assets/stylesheets/participacion.scss @@ -152,7 +152,6 @@ h1, h2, h3, h4, h5, h6 { } .f-dropdown { - li a { font-size: rem-calc(12); @@ -170,6 +169,19 @@ h1, h2, h3, h4, h5, h6 { } } +.margin { + margin-top: $line-height; + margin-bottom: $line-height; +} + +.margin-top { + margin-top: $line-height; +} + +.margin-bottom { + margin-bottom: $line-height; +} + // 04. Header // - - - - - - - - - - - - - - - - - - - - - - - - - @@ -514,9 +526,14 @@ footer { h2 { clear: both; - font-size: rem-calc(30); + font-size: rem-calc(18); font-weight: bold; - line-height: $line-height*2; + line-height: $line-height; + + @media (min-width: $small-breakpoint) { + font-size: rem-calc(30); + line-height: $line-height*2; + } } .back, .icon-angle-left { diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss index 64861e215..6240702d2 100644 --- a/app/assets/stylesheets/variables.scss +++ b/app/assets/stylesheets/variables.scss @@ -85,6 +85,7 @@ $comment-official: rgba(70,219,145,.3); // 06. Responsive // - - - - - - - - - - - - - - - - - - - - - - - - - +$small: em-calc(480); $small-breakpoint: em-calc(640); $medium-breakpoint: em-calc(1024); $large-breakpoint: em-calc(1440); 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/organizations/registrations_controller.rb b/app/controllers/organizations/registrations_controller.rb index 8602ba4ba..630cc64e4 100644 --- a/app/controllers/organizations/registrations_controller.rb +++ b/app/controllers/organizations/registrations_controller.rb @@ -5,6 +5,9 @@ class Organizations::RegistrationsController < Devise::RegistrationsController end end + def success + end + def create build_resource(sign_up_params) if resource.valid_with_captcha? @@ -17,6 +20,11 @@ class Organizations::RegistrationsController < Devise::RegistrationsController end end + protected + def after_inactive_sign_up_path_for(resource) + organizations_sign_up_success_path + end + private def sign_up_params diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..8588ba243 --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,29 @@ +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| + 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..4ce5998c8 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,19 @@ 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 + else + render :finish_signup + end + end + private def sign_up_params diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 000000000..3ba19e3fa --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,17 @@ +class Identity < ActiveRecord::Base + belongs_to :user + + validates :provider, presence: true + validates :uid, presence: true, uniqueness: { scope: :provider } + + def self.find_for_oauth(auth) + where(uid: auth.uid, provider: auth.provider).first_or_create + end + + def update_user(new_user) + return unless user != new_user + + self.user = new_user + save! + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 544142c2f..7cff8d732 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,43 @@ 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 + user ||= first_or_create_for_oauth(auth) + + # Associate the identity with the user if needed + identity.update_user(user) + user + end + + # 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 + def self.first_or_create_for_oauth(auth) + email = auth.info.email if auth.info.verified || auth.info.verified_email + 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 + + user + end + def name organization? ? organization.name : username end @@ -67,4 +110,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/account/show.html.erb b/app/views/account/show.html.erb index d24343ea5..fa9ccdd04 100644 --- a/app/views/account/show.html.erb +++ b/app/views/account/show.html.erb @@ -26,7 +26,7 @@

<%= t("account.show.avatar")%>

- <%= avatar_image(@account, size: 100) %> + <%= avatar_image(@account, seed: @account.id, size: 100) %>

<%= t("account.show.notifications")%>

diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 6f93bdde4..1b6f713ba 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -14,7 +14,7 @@ <% if comment.user.organization? %> <%= image_tag("collective_avatar.png", size: 32, class: "avatar left") %> <% else %> - <%= avatar_image(comment.user, size: 32, class: "left") %> + <%= avatar_image(comment.user, seed: comment.user_id, size: 32, class: "left") %> <% end %> <% if comment.user.hidden? %> diff --git a/app/views/debates/show.html.erb b/app/views/debates/show.html.erb index a6765e1b0..3b91edc8d 100644 --- a/app/views/debates/show.html.erb +++ b/app/views/debates/show.html.erb @@ -13,7 +13,7 @@

<%= @debate.title %>

- <%= avatar_image(@debate.author, size: 32, class: 'author-photo') %> + <%= avatar_image(@debate.author, seed: @debate.author_id, size: 32, class: 'author-photo') %> <% if @debate.author.hidden? %> diff --git a/app/views/devise/_omniauth_form.html.erb b/app/views/devise/_omniauth_form.html.erb new file mode 100644 index 000000000..fdd812b4f --- /dev/null +++ b/app/views/devise/_omniauth_form.html.erb @@ -0,0 +1,7 @@ +
+ +<%= link_to t("omniauth.twitter.sign_in"), user_omniauth_authorize_path(:twitter), class: 'button radius expand' %> +<%= link_to t("omniauth.facebook.sign_in"), user_omniauth_authorize_path(:facebook), class: 'button radius expand' %> +<%= link_to t("omniauth.google_oauth2.sign_in"), user_omniauth_authorize_path(:google_oauth2), 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/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index 0fd80f429..7f8142144 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -25,7 +25,7 @@ <%= link_to t("devise_views.shared.links.new_unlock"), new_unlock_path(resource_name) %>
    <% end -%> - <%- if devise_mapping.omniauthable? %> + <%- if devise_mapping.omniauthable? && devise_mapping.name == 'user' %> <%- resource_class.omniauth_providers.each do |provider| %> <%= link_to t("devise_views.shared.links.signin_with_provider", provider: provider.to_s.titleize), omniauth_authorize_path(resource_name, provider) %>
    <% end -%> diff --git a/app/views/organizations/registrations/success.html.erb b/app/views/organizations/registrations/success.html.erb new file mode 100644 index 000000000..e246365f5 --- /dev/null +++ b/app/views/organizations/registrations/success.html.erb @@ -0,0 +1,15 @@ +
    +
    +
    +

    <%= t("devise_views.organizations.registrations.success.title") %>

    +

    <%= t("devise_views.organizations.registrations.success.thank_you") %>

    +

    <%= t("devise_views.organizations.registrations.success.instructions_1_html") %>

    +

    <%= t("devise_views.organizations.registrations.success.instructions_2_html") %>

    +

    <%= t("devise_views.organizations.registrations.success.instructions_3_html") %>

    +

    + <%= link_to t("devise_views.organizations.registrations.success.back_to_index"), + root_path, class: "button radius small margin-top" %> +

    +
    +
    +
    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/deploy/shared/secrets.yml.erb b/config/deploy/shared/secrets.yml.erb index a1cc29fb8..42a8c48a6 100644 --- a/config/deploy/shared/secrets.yml.erb +++ b/config/deploy/shared/secrets.yml.erb @@ -2,4 +2,10 @@ recaptcha_public_key: <%= ENV["MADRID_RECAPTCHA_PUBLIC_KEY"] %> recaptcha_private_key: <%= ENV["MADRID_RECAPTCHA_PRIVATE_KEY"] %> secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> - server_name: <%= fetch(:server_name) %> \ No newline at end of file + twitter_key: <%= ENV["TWITTER_KEY"] %> + twitter_secret: <%= ENV["TWITTER_SECRET"] %> + facebook_key: <%= ENV["FACEBOOK_KEY"] %> + facebook_secret: <%= ENV["FACEBOOK_SECRET"] %> + google_oauth2_key: <%= ENV["GOOGLE_KEY"] %> + google_oauth2_secret: <%= ENV["GOOGLE_SECRET"] %> + server_name: <%= fetch(:server_name) %> diff --git a/config/environments/production.rb b/config/environments/production.rb index e84606f73..7a21a1df4 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -42,7 +42,7 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. diff --git a/config/environments/staging.rb b/config/environments/staging.rb index e84606f73..7a21a1df4 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -42,7 +42,7 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f7cdfb292..40f3e50b3 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -239,6 +239,9 @@ 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 + config.omniauth :facebook, Rails.application.secrets.facebook_key, Rails.application.secrets.facebook_secret + config.omniauth :google_oauth2, Rails.application.secrets.google_oauth2_key, Rails.application.secrets.google_oauth2_secret # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or 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/devise_views.en.yml b/config/locales/devise_views.en.yml index e1b2a571d..f0e1eb33a 100644 --- a/config/locales/devise_views.en.yml +++ b/config/locales/devise_views.en.yml @@ -66,6 +66,13 @@ en: phone_number_label: "Phone number" password_confirmation_label: "Confirm password" submit: "Sign up" + success: + title: "Registration of organization / collective" + thank_you: "Thank you for registering your organization or collective in the website. Now is pending verification." + instructions_1_html: "We will contact you soon in order to verify that you represent your collective." + instructions_2_html: "Meanwhile, review your email. We have sent you a confirmation link to activate your account." + instructions_3_html: "When you confirm your account will then be able to participate as a non-verified organization." + back_to_index: "Ok, go back to index" sessions: new: title: "Log in" diff --git a/config/locales/devise_views.es.yml b/config/locales/devise_views.es.yml index 7ed5298de..49d304748 100644 --- a/config/locales/devise_views.es.yml +++ b/config/locales/devise_views.es.yml @@ -66,6 +66,13 @@ es: phone_number_label: "Teléfono" password_confirmation_label: "Confirmar contraseña" submit: "Registrarse" + success: + title: "Registro de organización / colectivo" + thank_you: "Gracias por registrar tu colectivo en la web. Ahora está pendiente de verificación." + instructions_1_html: "En breve nos pondremos en contacto contigo para verificar que realmente representas a este colectivo." + instructions_2_html: "Mientras revisa tu correo electrónico, te hemos enviado un enlace para confirmar tu cuenta." + instructions_3_html: "Una vez confirmado, podrás empezar a participar como colectivo no verificado." + back_to_index: "Entendido, volver a la página principal" sessions: new: title: "Entrar" diff --git a/config/locales/en.yml b/config/locales/en.yml index 98ac387fc..857abb1b8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -148,3 +148,12 @@ 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 + facebook: + sign_in: Sign in with Facebook + google_oauth2: + sign_in: Sign in with Google diff --git a/config/locales/es.yml b/config/locales/es.yml index d548f2334..2ac4f0d9f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -148,3 +148,12 @@ 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: Entra con Twitter + facebook: + sign_in: Entra con Facebook + google_oauth2: + sign_in: Entra con Google diff --git a/config/routes.rb b/config/routes.rb index f224609b9..1e8b860a0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,23 @@ 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 :organization do + get "organizations/sign_up/success", to: "organizations/registrations#success" + end + + 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". diff --git a/config/secrets.yml.example b/config/secrets.yml.example index f4ad98027..1d1312ea6 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -14,12 +14,30 @@ default: &default development: secret_key_base: 56792feef405a59b18ea7db57b4777e855103882b926413d4afdfb8c0ea8aa86ea6649da4e729c5f5ae324c0ab9338f789174cf48c544173bc18fdc3b14262e4 + twitter_key: AAAA + twitter_secret: BBBB + facebook_key: AAAA + facebook_secret: BBBB + google_oauth2_key: AAAA + google_oauth2_secret: BBBB <<: *default test: secret_key_base: 4d5adf961ddd27aef19622d6c0b3234d555f9ee003f022b1f829c92bbe33aaee907be7feb67bd54c14a1a32512fa968565ad405971fbc41bd0797af73c26a796 + twitter_key: AAAA + twitter_secret: BBBB + facebook_key: AAAA + facebook_secret: BBBB + google_oauth2_key: AAAA + google_oauth2_secret: BBBB <<: *default production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> - <<: *default \ No newline at end of file + twitter_key: <%= ENV["TWITTER_KEY"] %> + twitter_secret: <%= ENV["TWITTER_SECRET"] %> + facebook_key: <%= ENV["FACEBOOK_KEY"] %> + facebook_secret: <%= ENV["FACEBOOK_SECRET"] %> + google_oauth2_key: <%= ENV["GOOGLE_KEY"] %> + google_oauth2_secret: <%= ENV["GOOGLE_SECRET"] %> + <<: *default diff --git a/db/migrate/20150824144524_create_identities.rb b/db/migrate/20150824144524_create_identities.rb new file mode 100644 index 000000000..38a5e603a --- /dev/null +++ b/db/migrate/20150824144524_create_identities.rb @@ -0,0 +1,11 @@ +class CreateIdentities < ActiveRecord::Migration + def change + create_table :identities do |t| + t.references :user, index: true, foreign_key: true + t.string :provider + t.string :uid + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 12e457a1c..d21459719 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150824113326) do +ActiveRecord::Schema.define(version: 20150824144524) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -66,8 +66,8 @@ ActiveRecord::Schema.define(version: 20150824113326) do t.integer "author_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.datetime "hidden_at" t.string "visit_id" + t.datetime "hidden_at" t.datetime "flagged_as_inappropiate_at" t.integer "inappropiate_flags_count", default: 0 t.datetime "reviewed_at" @@ -75,6 +75,16 @@ ActiveRecord::Schema.define(version: 20150824113326) do add_index "debates", ["hidden_at"], name: "index_debates_on_hidden_at", using: :btree + create_table "identities", force: :cascade do |t| + t.integer "user_id" + t.string "provider" + t.string "uid" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree + create_table "inappropiate_flags", force: :cascade do |t| t.integer "user_id" t.string "flaggable_type" @@ -156,10 +166,10 @@ ActiveRecord::Schema.define(version: 20150824113326) do t.string "unconfirmed_email" t.boolean "email_on_debate_comment", default: false t.boolean "email_on_comment_reply", default: false + t.string "phone_number", limit: 30 t.string "official_position" t.integer "official_level", default: 0 t.datetime "hidden_at" - t.string "phone_number", limit: 30 t.string "username" end @@ -214,6 +224,7 @@ ActiveRecord::Schema.define(version: 20150824113326) do add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree add_foreign_key "administrators", "users" + add_foreign_key "identities", "users" add_foreign_key "inappropiate_flags", "users" add_foreign_key "moderators", "users" add_foreign_key "organizations", "users" diff --git a/spec/factories.rb b/spec/factories.rb index 038a0102e..4fa2a70e3 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,5 +1,4 @@ FactoryGirl.define do - factory :user do username 'Manuela' sequence(:email) { |n| "manuela#{n}@madrid.es" } @@ -7,6 +6,12 @@ FactoryGirl.define do confirmed_at { Time.now } end + factory :identity do + user nil + provider "Twitter" + uid "MyString" + end + factory :debate do sequence(:title) { |n| "Debate #{n} title" } description 'Debate description' diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 42511f28c..1fa30bcbe 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -2,44 +2,159 @@ require 'rails_helper' feature 'Users' do - scenario 'Sign up' do - visit '/' - click_link 'Sign up' + context 'Regular authentication' do + scenario 'Sign up' do + visit '/' + click_link 'Sign up' - fill_in 'user_username', with: 'Manuela Carmena' - fill_in 'user_email', with: 'manuela@madrid.es' - fill_in 'user_password', with: 'judgementday' - fill_in 'user_password_confirmation', with: 'judgementday' - fill_in 'user_captcha', with: correct_captcha_text + fill_in 'user_username', with: 'Manuela Carmena' + fill_in 'user_email', with: 'manuela@madrid.es' + fill_in 'user_password', with: 'judgementday' + fill_in 'user_password_confirmation', with: 'judgementday' + fill_in 'user_captcha', with: correct_captcha_text - click_button 'Sign up' + click_button 'Sign up' - expect(page).to have_content "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + expect(page).to have_content "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." - sent_token = /.*confirmation_token=(.*)".*/.match(ActionMailer::Base.deliveries.last.body.to_s)[1] - visit user_confirmation_path(confirmation_token: sent_token) + sent_token = /.*confirmation_token=(.*)".*/.match(ActionMailer::Base.deliveries.last.body.to_s)[1] + visit user_confirmation_path(confirmation_token: sent_token) - expect(page).to have_content "Your email address has been successfully confirmed" + expect(page).to have_content "Your email address has been successfully confirmed" + end + + scenario 'Errors on sign up' do + visit '/' + click_link 'Sign up' + click_button 'Sign up' + + expect(page).to have_content error_message + end + + scenario 'Sign in' do + create(:user, email: 'manuela@madrid.es', password: 'judgementday') + + visit '/' + click_link 'Log in' + fill_in 'user_email', with: 'manuela@madrid.es' + fill_in 'user_password', with: 'judgementday' + click_button 'Log in' + + expect(page).to have_content 'Signed in successfully.' + end end - scenario 'Errors on sign up' do - visit '/' - click_link 'Sign up' - click_button 'Sign up' + context 'OAuth authentication' do + context 'Twitter' do + background do + #request.env["devise.mapping"] = Devise.mappings[:user] + end - expect(page).to have_content error_message - end + scenario 'Sign up, when email was provided by OAuth provider' do + omniauth_twitter_hash = { 'provider' => 'twitter', + 'uid' => '12345', + 'info' => { + 'name' => 'manuela', + 'email' => 'manuelacarmena@example.com', + 'nickname' => 'ManuelaRocks', + 'verified' => '1' + }, + 'extra' => { 'raw_info' => + { 'location' => 'Madrid', + 'name' => 'Manuela de las Carmenas' + } + } + } - scenario 'Sign in' do - create(:user, email: 'manuela@madrid.es', password: 'judgementday') + OmniAuth.config.add_mock(:twitter, omniauth_twitter_hash) - visit '/' - click_link 'Log in' - fill_in 'user_email', with: 'manuela@madrid.es' - fill_in 'user_password', with: 'judgementday' - click_button 'Log in' + visit '/' + click_link 'Sign up' - expect(page).to have_content 'Signed in successfully.' + expect do + expect do + expect do + click_link 'Sign in with Twitter' + end.not_to change { ActionMailer::Base.deliveries.size } + end.to change { Identity.count }.by(1) + end.to change { User.count }.by(1) + + expect(current_path).to eq(root_path) + expect_to_be_signed_in + + user = User.last + expect(user.username).to eq('ManuelaRocks') + expect(user.email).to eq('manuelacarmena@example.com') + expect(user.confirmed?).to eq(true) + end + + scenario 'Sign up, when neither email nor nickname were provided by OAuth provider' do + omniauth_twitter_hash = { 'provider' => 'twitter', + 'uid' => '12345', + 'info' => { + 'name' => 'manuela' + }, + 'extra' => { 'raw_info' => + { 'location' => 'Madrid', + 'name' => 'Manuela de las Carmenas' + } + } + } + + OmniAuth.config.add_mock(:twitter, omniauth_twitter_hash) + + visit '/' + click_link 'Sign up' + + expect do + expect do + expect do + click_link 'Sign in with Twitter' + end.not_to change { ActionMailer::Base.deliveries.size } + end.to change { Identity.count }.by(1) + end.to change { User.count }.by(1) + + expect(current_path).to eq(finish_signup_path) + + user = User.last + expect(user.username).to eq('manuela-de-las-carmenas') + expect(user.email).to eq("omniauth@participacion-12345-twitter.com") + + fill_in 'user_email', with: 'manueladelascarmenas@example.com' + click_button 'Sign up' + + sent_token = /.*confirmation_token=(.*)".*/.match(ActionMailer::Base.deliveries.last.body.to_s)[1] + visit user_confirmation_path(confirmation_token: sent_token) + + expect(page).to have_content "Your email address has been successfully confirmed" + + expect(user.reload.email).to eq('manueladelascarmenas@example.com') + end + + scenario 'Sign in, user was already signed up with OAuth' do + user = create(:user, email: 'manuela@madrid.es', password: 'judgementday') + identity = create(:identity, uid: '12345', provider: 'twitter', user: user) + omniauth_twitter_hash = { 'provider' => 'twitter', + 'uid' => '12345', + 'info' => { + 'name' => 'manuela' + } + } + + OmniAuth.config.add_mock(:twitter, omniauth_twitter_hash) + + visit '/' + click_link 'Log in' + + expect do + expect do + click_link 'Sign in with Twitter' + end.not_to change { Identity.count } + end.not_to change { User.count } + + expect_to_be_signed_in + end + end end scenario 'Sign out' do @@ -73,5 +188,4 @@ feature 'Users' do expect(page).to have_content "Your password has been changed successfully. You are now signed in." end - end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb new file mode 100644 index 000000000..82d5c4be3 --- /dev/null +++ b/spec/models/identity_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' + +RSpec.describe Identity, type: :model do + let(:identity) { build(:identity) } + + it "should be valid" do + expect(identity).to be_valid + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5a4bbdf88..e0260285a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -50,6 +50,26 @@ describe User do end end + describe 'OmniAuth' do + describe '#email_provided?' do + it "is false if the email matchs was temporarely assigned by the OmniAuth process" do + subject.email = 'omniauth@participacion-ABCD-twitter.com' + expect(subject.email_provided?).to eq(false) + end + + it "is true if the email is not omniauth-like" do + subject.email = 'manuelacarmena@example.com' + expect(subject.email_provided?).to eq(true) + end + + it "is true if the user's real email is pending to be confirmed" do + subject.email = 'omniauth@participacion-ABCD-twitter.com' + subject.unconfirmed_email = 'manuelacarmena@example.com' + expect(subject.email_provided?).to eq(true) + end + end + end + describe "administrator?" do it "is false when the user is not an admin" do expect(subject.administrator?).to be false diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d073cc173..c2f5db0fe 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -22,3 +22,5 @@ RSpec.configure do |config| end Capybara.javascript_driver = :poltergeist + +OmniAuth.config.test_mode = true diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index 21a98f56f..12d6affd3 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -77,4 +77,7 @@ module CommonActions /\d errors? prohibited this (.*) from being saved:/ end + def expect_to_be_signed_in + expect(find('.top-bar')).to have_content 'My account' + end end