Skip to content

Commit

Permalink
Merge pull request #16 from debtcollective/od/auth-v2
Browse files Browse the repository at this point in the history
Redirect users to a specific URL on their first email login
  • Loading branch information
orlando authored Feb 18, 2021
2 parents 49acc7d + d1e9ded commit 8a2c97c
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 58 deletions.
2 changes: 1 addition & 1 deletion config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ en:
debtcollective_algolia_app_id: 'Algolia Places App ID'
debtcollective_algolia_api_key: 'Algolia Places API key'
debtcollective_algolia_api_key: 'Algolia Places API key'
debtcollective_redirect_url_after_accept_invitation: 'Redirect URL after user accepts an invite. ex https://debtcollective.org/hub'
debtcollective_after_signup_redirect_url: 'Redirect URL after user accepts an invite. ex https://debtcollective.org/hub'
20 changes: 10 additions & 10 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@ plugins:
enable_debtcollective_sso:
default: true
sso_cookie_domain:
default: '.lvh.me'
default: ".lvh.me"
shadowed_by_global: true
sso_cookie_name:
default: 'tdc_auth_token'
default: "tdc_auth_token"
shadowed_by_global: true
sso_jwt_secret:
default: 'changeme'
default: "changeme"
shadowed_by_global: true
debtcollective_algolia_app_id:
default: ''
default: ""
shadowed_by_global: true
debtcollective_algolia_api_key:
default: ''
default: ""
shadowed_by_global: true
debtcollective_solidarity_message_author:
default: 'system'
default: "system"
client: false
debtcollective_solidarity_message_title:
default: 'Solidarity Bloc'
client: false
debtcollective_redirect_url_after_accept_invitation:
default: 'http://lvh.me:8000/hub'
default: "Solidarity Bloc"
client: false
debtcollective_after_signup_redirect_url:
default: "https://debtcollective.org/hub"
client: true
5 changes: 2 additions & 3 deletions lib/extensions/invites_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ def perform_accept_invitation

# If this is a new user or first login, redirect
# we only set it if topic invite is nil
redirect_to = SiteSetting.debtcollective_redirect_url_after_accept_invitation

redirect_to = SiteSetting.debtcollective_after_signup_redirect_url
if (user.new_user? || !user.seen_before?) && redirect_to.present? && topic.blank?
response[:redirect_to] = SiteSetting.debtcollective_redirect_url_after_accept_invitation
response[:redirect_to] = redirect_to
end
elsif user.present?
response[:message] = I18n.t('invite.confirm_email')
Expand Down
33 changes: 33 additions & 0 deletions lib/extensions/session_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,39 @@ def sso_provider(payload = nil)
end
end

# We override this method to make the redirection on the first login
def email_login
token = params[:token]
matched_token = EmailToken.confirmable(token)
user = matched_token&.user

check_local_login_allowed(user: user, check_login_via_email: true)

if user.present? && !authenticate_second_factor(user)
return render(json: @second_factor_failure_payload)
end

if user = EmailToken.confirm(token)
if login_not_approved_for?(user)
return render json: login_not_approved
elsif payload = login_error_check(user)
return render json: payload
else
user.update_timezone_if_missing(params[:timezone])
log_on_user(user)

response = { success: 'OK' }

redirect_to = SiteSetting.debtcollective_after_signup_redirect_url
response[:redirect_to] = redirect_to if redirect_to.present? && !user.seen_before?

return render json: response
end
end

render json: { error: I18n.t('email_login.invalid_token') }
end

private

def check_return_url
Expand Down
163 changes: 163 additions & 0 deletions lib/extensions/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,169 @@ def perform_account_activation

render layout: 'no_ember'
end

def create
params.require(:email)
params.require(:invite_code) if SiteSetting.require_invite_code
params.permit(:user_fields)

unless SiteSetting.allow_new_registrations
return fail_with("login.new_registrations_disabled")
end

if params[:password] && params[:password].length > User.max_password_length
return fail_with("login.password_too_long")
end

if params[:email].length > 254 + 1 + 253
return fail_with("login.email_too_long")
end

if SiteSetting.require_invite_code && SiteSetting.invite_code.strip.downcase != params[:invite_code].strip.downcase
return fail_with("login.wrong_invite_code")
end

# Generate username if it's empty and it's an API call
# We use to create user accounts from the Membership app
if is_api? && params[:username].blank?
params[:username] = UserNameSuggester.suggest(user_params[:email])
end

# defered username check
params.require(:username)

if clashing_with_existing_route?(params[:username]) || User.reserved_username?(params[:username])
return fail_with("login.reserved_username")
end

params[:locale] ||= I18n.locale unless current_user

new_user_params = user_params.except(:timezone)

user = User.where(staged: true).with_email(new_user_params[:email].strip.downcase).first

if user
user.active = false
user.unstage!
end

user ||= User.new
user.attributes = new_user_params

# Handle API approval and
# auto approve users based on auto_approve_email_domains setting
if user.approved? || EmailValidator.can_auto_approve_user?(user.email)
ReviewableUser.set_approved_fields!(user, current_user)
end

# Handle custom fields
user_fields = UserField.all
if user_fields.present?
field_params = params[:user_fields] || {}
fields = user.custom_fields

user_fields.each do |f|
field_val = field_params[f.id.to_s]
if field_val.blank?
return fail_with("login.missing_user_field") if f.required?
else
fields["#{User::USER_FIELD_PREFIX}#{f.id}"] = field_val[0...UserField.max_length]
end
end

user.custom_fields = fields
end

authentication = UserAuthenticator.new(user, session)

if !authentication.has_authenticator? && !SiteSetting.enable_local_logins && !(current_user&.admin? && is_api?)
return render body: nil, status: :forbidden
end

authentication.start

if authentication.email_valid? && !authentication.authenticated?
# posted email is different that the already validated one?
return fail_with('login.incorrect_username_email_or_password')
end

activation = UserActivator.new(user, request, session, cookies)
activation.start

# just assign a password if we have an authenticator and no password
# this is the case for Twitter
user.password = SecureRandom.hex if user.password.blank? && authentication.has_authenticator?

if user.save
authentication.finish
activation.finish
user.update_timezone_if_missing(params[:timezone])

secure_session[::ApplicationController::HONEYPOT_KEY] = nil
secure_session[::ApplicationController::CHALLENGE_KEY] = nil

# save user email in session, to show on account-created page
session["user_created_message"] = activation.message
session[::SessionController::ACTIVATE_USER_KEY] = user.id

# If the user was created as active this will
# ensure their email is confirmed and
# add them to the review queue if they need to be approved
user.activate if user.active?

response = {
success: true,
active: user.active?,
message: activation.message,
user_id: user.id
}

if is_api?
# Create a signin link to be used to login the user for the first time.
email_token = user.email_tokens.create!(email: user.email)

response[:email_token] = email_token.token
end

render json: response
elsif SiteSetting.hide_email_address_taken && user.errors[:primary_email]&.include?(I18n.t('errors.messages.taken'))
session["user_created_message"] = activation.success_message

if existing_user = User.find_by_email(user.primary_email&.email)
Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id)
end

render json: {
success: true,
active: user.active?,
message: activation.success_message,
user_id: user.id
}
else
errors = user.errors.to_hash
errors[:email] = errors.delete(:primary_email) if errors[:primary_email]

render json: {
success: false,
message: I18n.t(
'login.errors',
errors: user.errors.full_messages.join("\n")
),
errors: errors,
values: {
name: user.name,
username: user.username,
email: user.primary_email&.email
},
is_developer: UsernameCheckerService.is_developer?(user.email)
}
end
rescue ActiveRecord::StatementInvalid
render json: {
success: false,
message: I18n.t("login.something_already_taken")
}
end
end

::UsersController.class_eval do
Expand Down
44 changes: 0 additions & 44 deletions spec/requests/collectives_controller_spec.rb

This file was deleted.

15 changes: 15 additions & 0 deletions spec/requests/session_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,19 @@
expect(response).to redirect_to('/signup')
end
end

describe 'GET email_login' do
it "redirects to after_signup_path if it's the first login" do
user = Fabricate(:user)
email_token = user.email_tokens.create!(email: user.email)
SiteSetting.debtcollective_after_signup_redirect_url = "https://example.com"

post "/session/email-login/#{email_token.token}", headers: { "ACCEPT" => "application/json" }
json = JSON.parse(response.body)

expect(json["success"]).to eq("OK")
expect(json["redirect_to"]).to eq("https://example.com")
expect(response.status).to eq(200)
end
end
end

0 comments on commit 8a2c97c

Please sign in to comment.