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