diff --git a/Gemfile b/Gemfile index ff658fde3..1bea45cc0 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'font-awesome-rails', '~> 4.7.0.2' gem 'recaptcha', require: 'recaptcha/rails' gem 'sanitize', '~> 4.4' gem 'omniauth' +gem 'omniauth-linkedin-oauth2' # Gems for API implementation gem 'jwt', '~> 1.5' diff --git a/app/assets/images/linkedin/Sign-in-Small---Active.png b/app/assets/images/linkedin/Sign-in-Small---Active.png new file mode 100644 index 000000000..dea692fd9 Binary files /dev/null and b/app/assets/images/linkedin/Sign-in-Small---Active.png differ diff --git a/app/assets/images/linkedin/Sign-in-Small---Default.png b/app/assets/images/linkedin/Sign-in-Small---Default.png new file mode 100644 index 000000000..f8129afe2 Binary files /dev/null and b/app/assets/images/linkedin/Sign-in-Small---Default.png differ diff --git a/app/assets/images/linkedin/Sign-in-Small---Hover.png b/app/assets/images/linkedin/Sign-in-Small---Hover.png new file mode 100644 index 000000000..643236eec Binary files /dev/null and b/app/assets/images/linkedin/Sign-in-Small---Hover.png differ diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 27b884710..7b0bff2b9 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,6 +1,10 @@ module Users class OmniauthCallbacksController < Devise::OmniauthCallbacksController + include UsersGenerator + skip_before_action :verify_authenticity_token + before_action :sign_up_with_provider_enabled?, + only: :linkedin # You should configure your model like this: # devise :omniauthable, omniauth_providers: [:twitter] @@ -9,6 +13,55 @@ module Users # def twitter # end + def linkedin + auth_hash = request.env['omniauth.auth'] + + @user = User.from_omniauth(auth_hash) + if @user && @user.current_team_id? + # User already exists and has been signed up with LinkedIn; just sign in + set_flash_message(:notice, + :success, + kind: I18n.t('devise.linkedin.provider_name')) + sign_in_and_redirect @user + elsif @user + # User already exists and has started sign up with LinkedIn; + # but doesn't have team (needs to complete sign up - agrees to TOS) + set_flash_message(:alert, + :failure, + kind: I18n.t('devise.linkedin.provider_name'), + reason: I18n.t('devise.linkedin.complete_sign_up')) + redirect_to users_sign_up_provider_path(user: @user) + elsif User.find_by_email(auth_hash['info']['email']) + # email is already taken, so sign up with Linked in is not allowed + set_flash_message(:alert, + :failure, + kind: I18n.t('devise.linkedin.provider_name'), + reason: I18n.t('devise.linkedin.email_already_taken', + email: auth_hash['info']['email'])) + redirect_to after_omniauth_failure_path_for(resource_name) + else + # Create new user and identity; and redirect to complete sign up form + @user = User.new( + full_name: auth_hash['info']['name'], + initials: generate_initials(auth_hash['info']['name']), + email: auth_hash['info']['email'], + password: generate_user_password + ) + @user.avatar_remote_url = (auth_hash['info']['image']) + user_identity = UserIdentity.new(user: @user, + provider: auth_hash['provider'], + uid: auth_hash['uid']) + unless @user.save && user_identity.save + set_flash_message(:alert, + :failure, + kind: I18n.t('devise.linkedin.provider_name'), + reason: I18n.t('devise.linkedin.failed_to_save')) + redirect_to after_omniauth_failure_path_for(resource_name) and return + end + redirect_to users_sign_up_provider_path(user: @user) + end + end + # More info at: # https://github.com/plataformatec/devise#omniauth @@ -28,5 +81,18 @@ module Users # def after_omniauth_failure_path_for(scope) # super(scope) # end + + private + + def sign_up_with_provider_enabled? + render_403 unless Rails.configuration.x.enable_user_registration + render_403 unless Rails.configuration.x.linkedin_signin_enabled + end + + def generate_initials(full_name) + initials = full_name.titleize.scan(/[A-Z]+/).join + initials = initials.strip.empty? ? 'PLCH' : initials[0..3] + initials + end end end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index dbda51b4a..f127dfd13 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,5 +1,9 @@ class Users::RegistrationsController < Devise::RegistrationsController prepend_before_action :check_captcha, only: [:create] + before_action :registration_enabled?, + only: %i(new create new_with_provider create_with_provider) + before_action :sign_up_with_provider_enabled?, + only: %i(new_with_provider create_with_provider) def avatar user = User.find_by_id(params[:id]) || current_user @@ -122,12 +126,9 @@ class Users::RegistrationsController < Devise::RegistrationsController end end - def new - render_403 && return unless Rails.configuration.x.enable_user_registration - end + def new; end def create - render_403 && return unless Rails.configuration.x.enable_user_registration build_resource(sign_up_params) valid_resource = resource.valid? # ugly checking if new team on sign up is enabled :( @@ -174,6 +175,36 @@ class Users::RegistrationsController < Devise::RegistrationsController end end + def new_with_provider; end + + def create_with_provider + @user = User.find_by_id(user_provider_params['user']) + # Create new team for the new user + @team = Team.new(team_provider_params) + @team.validate + + if @team.valid? && Rails.configuration.x.new_team_on_signup + # Set the confirmed_at == created_at IF not using email confirmations + unless Rails.configuration.x.enable_email_confirmations + @user.update!(confirmed_at: @user.created_at) + end + + @team.created_by = @user # set created_by for team + @team.save! + + # Add this user to the team as owner + UserTeam.create(user: @user, team: @team, role: :admin) + + # set current team to new user + @user.current_team_id = @team.id + @user.save! + + sign_in_and_redirect @user + else + render :new_with_provider + end + end + protected # Called upon creating User (before .save). Permits parameters and extracts @@ -191,6 +222,14 @@ class Users::RegistrationsController < Devise::RegistrationsController tmp.merge(:initials => initials) end + def team_provider_params + params.require(:team).permit(:name) + end + + def user_provider_params + params.permit(:user) + end + def account_update_params params.require(:user).permit( :full_name, @@ -268,6 +307,14 @@ class Users::RegistrationsController < Devise::RegistrationsController end end + def registration_enabled? + render_403 unless Rails.configuration.x.enable_user_registration + end + + def sign_up_with_provider_enabled? + render_403 unless Rails.configuration.x.linkedin_signin_enabled + end + # Redirect to login page after signing up def after_sign_up_path_for(resource) new_user_session_path diff --git a/app/models/user.rb b/app/models/user.rb index e9265fbc6..cca3b9409 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -217,6 +217,14 @@ class User < ApplicationRecord self.full_name = name end + def avatar_remote_url=(url_value) + self.avatar = URI.parse(url_value) + # Assuming url_value is http://example.com/photos/face.png + # avatar_file_name == "face.png" + # avatar_content_type == "image/png" + @avatar_remote_url = url_value + end + def current_team Team.find_by_id(self.current_team_id) end diff --git a/app/views/users/registrations/new_with_provider.html.erb b/app/views/users/registrations/new_with_provider.html.erb new file mode 100644 index 000000000..2feddd300 --- /dev/null +++ b/app/views/users/registrations/new_with_provider.html.erb @@ -0,0 +1,37 @@ +<% provide(:head_title, t('users.registrations.new.head_title')) %> + +
+

<%= t 'users.registrations.new_with_provider.head_title' %>

+
+ <%= form_for(:team, as: resource_name, url: users_complete_sign_up_provider_path, html: { id: "sign-up-provider-form" } ) do |f| %> + <%= hidden_field_tag :user, params['user'] %> + + <% if Rails.configuration.x.new_team_on_signup %> +
+ <%= f.label :name, t('users.registrations.new.team_name_label') %> + <%= f.text_field :name, autofocus: true, class: 'form-control' %> + <%= t 'users.registrations.new.team_name_help' %> +
+ <% end %> + +
+ <%= f.submit 'Sign up', class: 'btn btn-primary' %> +
+ <% end %> +
+ <%= render 'users/shared/links' %> +
+ +<% if @team and not @team.errors.empty? %> + +<% end %> diff --git a/app/views/users/shared/_links.html.erb b/app/views/users/shared/_links.html.erb index c98abe826..f0d9d4fea 100644 --- a/app/views/users/shared/_links.html.erb +++ b/app/views/users/shared/_links.html.erb @@ -1,5 +1,7 @@ <%- if controller_name != 'sessions' %> - <%= link_to t("devise.links.login"), new_session_path(resource_name) %>
+ <% login = t("devise.links.login") %> + <% login = t("devise.links.login_with_provider") if ['new_with_provider', 'create_with_provider'].include? action_name %> + <%= link_to login, new_session_path(resource_name) %>
<% end -%> <%- if devise_mapping.registerable? && Rails.configuration.x.enable_user_registration && controller_name != 'registrations' %> @@ -21,3 +23,14 @@ <%- if devise_mapping.omniauthable? && resource_class.omniauth_providers.any? %>
<% end -%> + +<%- if Rails.configuration.x.enable_user_registration && Rails.configuration.x.linkedin_signin_enabled %> + <%- if devise_mapping.omniauthable? && resource_class.omniauth_providers.any? && controller_name != 'registrations' %> + <%= link_to omniauth_authorize_path(resource_name, :linkedin), :title => "Sign in with LinkedIn" do %> + <%= image_tag 'linkedin/Sign-in-Small---Default.png', alt: "Sign in with LinkedIn", + onmouseover: "src='#{image_path('linkedin/Sign-in-Small---Hover.png')}'", + onmouseout: "src='#{image_path('linkedin/Sign-in-Small---Default.png')}'", + onclick: "src='#{image_path('linkedin/Sign-in-Small---Active.png')}'" %> + <% end -%> + <% end -%> +<% end -%> diff --git a/config/environments/development.rb b/config/environments/development.rb index 5f6bbe557..8ad2dc82a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -102,6 +102,9 @@ Rails.application.configure do config.x.enable_user_registration = ENV['ENABLE_USER_REGISTRATION'] == 'false' ? false : true + # Enable sign in with LinkedIn account + config.x.linkedin_signin_enabled = ENV['LINKEDIN_SIGNIN_ENABLED'] == 'true' + # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. #config.file_watcher = ActiveSupport::EventedFileUpdateChecker diff --git a/config/environments/production.rb b/config/environments/production.rb index 79504c7dc..21584572d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -116,6 +116,9 @@ Rails.application.configure do config.x.enable_user_registration = ENV['ENABLE_USER_REGISTRATION'] == 'false' ? false : true + # Enable sign in with LinkedIn account + config.x.linkedin_signin_enabled = ENV['LINKEDIN_SIGNIN_ENABLED'] == 'true' + # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index af1646826..3929886a6 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -287,6 +287,8 @@ 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 :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET'], + scope: %w(r_basicprofile r_emailaddress) # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index 709c25017..37b9e1282 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -41,7 +41,8 @@ class Extends # Data type name should match corresponding model's name REPOSITORY_DATA_TYPES = { RepositoryTextValue: 0, - RepositoryDateValue: 1 } + RepositoryDateValue: 1, + RepositoryListValue: 2 } # List of implemented core API versions API_VERSIONS = ['20170715'] @@ -49,7 +50,7 @@ class Extends # Array used for injecting names of additional authentication methods for API API_PLUGABLE_AUTH_METHODS = [] - OMNIAUTH_PROVIDERS = [] + OMNIAUTH_PROVIDERS = [:linkedin, *(:developer if Rails.env.development?)] INITIAL_USER_OPTIONS = {} end diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index c88f1dccc..392784c53 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -11,6 +11,8 @@ Paperclip::Attachment.default_options.merge!( url: '/system/:class/:attachment/:id_partition/:hash/:style/:filename' ) +Paperclip::UriAdapter.register + if ENV['PAPERCLIP_STORAGE'] == "s3" if ENV['S3_BUCKET'].nil? or ENV['AWS_REGION'].nil? or diff --git a/config/locales/en.yml b/config/locales/en.yml index 6db4618fa..f508e4616 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -40,11 +40,17 @@ en: submit: "Resend unlock instructions" links: login: "Log in" + login_with_provider: "Log in with SciNote account" signup: "Sign up" forgot: "Forgot your password?" not_receive_confirmation: "Didn't receive confirmation instructions?" not_receive_unlock: "Didn't receive unlock instructions?" sign_in_provider: "Sign in with %{provider}" + linkedin: + provider_name: "LinkedIn" + complete_sign_up: "You have to complete the sign up process" + email_already_taken: "SciNote account with email %{email} alreday exists" + failed_to_save: "Failed to create new user" helpers: label: @@ -1272,6 +1278,8 @@ en: head_title: "Sign up" team_name_label: "Team name" team_name_help: "Team name is required in order to create your own Team. After you create your own Team, you will be able to join other Teams as well." + new_with_provider: + head_title: "Complete the Sign up" statistics: title: "My statistics" team: "Team" diff --git a/config/routes.rb b/config/routes.rb index 5679d1157..1e7afbb88 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -498,6 +498,8 @@ Rails.application.routes.draw do get 'avatar/:id/:style' => 'users/registrations#avatar', as: 'avatar' post 'avatar_signature' => 'users/registrations#signature' get 'users/auth_token_sign_in' => 'users/sessions#auth_token_create' + get 'users/sign_up_provider' => 'users/registrations#new_with_provider' + post 'users/complete_sign_up_provider' => 'users/registrations#create_with_provider' end namespace :api, defaults: { format: 'json' } do diff --git a/lib/tasks/data.rake b/lib/tasks/data.rake index 44be9f9b8..ab102ada9 100644 --- a/lib/tasks/data.rake +++ b/lib/tasks/data.rake @@ -69,6 +69,12 @@ namespace :data do .where.not(invitation_token: nil) .where("created_at < ?", Devise.invite_for.ago) destroy_users(users) + + # Remove users who didn't finish signup with LinkedIn + users = User.joins(:user_identities) + .where(confirmed_at: nil) + #.where('created_at < ?', Devise.confirm_within.ago) + destroy_users(users) end desc "Remove temporary and obsolete data"