-
-
Notifications
You must be signed in to change notification settings - Fork 529
Feature 1792 - users can choose to recieve password resets via SMS #3757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f3ce7c8
83d1947
c7008cc
a2d8074
5b28758
f8dbab0
7c46dc1
c48b094
a6fa24a
ad6bf41
5ce13d0
ee24de3
cfa8434
f2db6b9
be2b80d
76d0327
09149e3
f5b9b6e
a90ec31
90c0468
ee1954e
b608904
a363799
45bcefe
3fc478d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| class Users::PasswordsController < Devise::PasswordsController | ||
| include ApplicationHelper | ||
| include PhoneNumberHelper | ||
| include SmsBodyHelper | ||
|
|
||
| def create | ||
| email, phone_number = [params[resource_name][:email], params[resource_name][:phone_number]] | ||
| @resource = email.blank? ? User.find_by(phone_number: phone_number) : User.find_by(email: email) | ||
|
|
||
| # re-render and display any errors | ||
| params_is_valid, error_resource = password_params_is_valid(resource, email, phone_number) | ||
| if !params_is_valid | ||
| respond_with(error_resource) | ||
| return | ||
| end | ||
|
|
||
| # generate a reset token and | ||
| # call devise mailer | ||
| reset_token = send_email_reset(email) | ||
| # for case where user enters ONLY a phone number, generate a new reset token to use; | ||
| # otherwise, use the same reset token as sent by devise mailer | ||
| send_sms_reset(@resource, phone_number, reset_token) | ||
| redirect_to after_sending_reset_password_instructions_path_for(resource_name), notice: "You will receive an email or SMS with instructions on how to reset your password in a few minutes." | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def send_email_reset(email) | ||
| reset_token = nil | ||
| if !email.blank? | ||
| reset_token = @resource.send_reset_password_instructions | ||
| end | ||
| reset_token | ||
| end | ||
|
|
||
| def send_sms_reset(resource, phone_number, reset_token) | ||
| if !phone_number.blank? | ||
| reset_token ||= resource.generate_password_reset_token | ||
| short_io_service = ShortUrlService.new | ||
| short_io_service.create_short_url(request.base_url + "/users/password/edit?reset_password_token=#{reset_token}") | ||
| twilio_service = TwilioService.new(resource.casa_org.twilio_api_key_sid, resource.casa_org.twilio_api_key_secret, resource.casa_org.twilio_account_sid) | ||
| sms_params = { | ||
| From: resource.casa_org.twilio_phone_number, | ||
| Body: password_reset_msg(resource.display_name, short_io_service.short_url), | ||
| To: phone_number | ||
| } | ||
| twilio_service.send_sms(sms_params) | ||
| end | ||
| end | ||
|
|
||
| def password_params_is_valid(resource, email, phone_number) | ||
| if email.blank? && phone_number.blank? | ||
| resource.errors.add(:base, "Please enter at least one field.") | ||
| return [false, resource] | ||
| end | ||
|
|
||
| phone_number_is_valid, error_message = valid_phone_number(phone_number) | ||
| if !phone_number_is_valid | ||
| resource.errors.add(:phone_number, error_message) | ||
| return [false, resource] | ||
| end | ||
|
|
||
| if resource.email != email || resource.phone_number != phone_number | ||
| # A new, empty resource is returned (see application helper) | ||
| # so to check for nil, we need to check its email/phone fields | ||
| resource.errors.add(:base, "User does not exist.") | ||
| return [false, resource] | ||
| end | ||
| [true, nil] | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,4 +44,16 @@ def flash_class(level) | |
| def og_tag(type, options = {}) | ||
| tag.meta(property: "og:#{type}", **options) | ||
| end | ||
|
|
||
| def resource_name | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for devise ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, for devise. The devise view calls some internal methods like resource and resource_name. So we need to map these to use them in our controllers and specs |
||
| :user | ||
| end | ||
|
|
||
| def resource | ||
| @resource ||= User.new | ||
| end | ||
|
|
||
| def devise_mapping | ||
| @devise_mapping ||= Devise.mappings[:user] | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| require "rails_helper" | ||
| require "support/stubbed_requests/webmock_helper" | ||
|
|
||
| RSpec.describe Users::PasswordsController, type: :controller do | ||
| describe "create" do | ||
| before do | ||
| stubbed_sites = ["api.twilio.com", "api.short.io"] | ||
| web_mock = WebMockHelper.new(stubbed_sites) | ||
| web_mock.stub_network_connection | ||
| end | ||
|
|
||
| it "sends a password reset SMS to existing user" do | ||
| org = create(:casa_org) | ||
| user = create(:user, phone_number: "+12222222222", casa_org: org) | ||
|
|
||
| @short_io_stub = WebMockHelper.short_io_stub_sms | ||
| @twilio_stub = WebMockHelper.twilio_password_reset_stub(user) | ||
|
|
||
| params = { | ||
| user: { | ||
| email: user.email, | ||
| phone_number: user.phone_number | ||
| } | ||
| } | ||
|
|
||
| post :create, params: params | ||
| expect(@short_io_stub).to have_been_requested.times(1) | ||
| expect(@twilio_stub).to have_been_requested.times(1) | ||
| expect(response).to have_http_status(:redirect) | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| require "rails_helper" | ||
|
|
||
| RSpec.describe "users/passwords/new", type: :system do | ||
| before do | ||
| visit root_path | ||
| click_on "Forgot your password?" | ||
| end | ||
|
|
||
| it "displays error messages for non-existent user" do | ||
| fill_in "Email", with: "tangerine@forward.com" | ||
| fill_in "Phone number", with: "+16578900012" | ||
|
|
||
| click_on "Send me reset password instructions" | ||
| expect(page).to have_content "1 error prohibited this User from being saved:" | ||
| expect(page).to have_text("User does not exist.") | ||
| end | ||
|
|
||
| it "displays phone number error messages for incorrect formatting" do | ||
| create(:user, email: "glados@aperture.labs") | ||
| fill_in "Email", with: "glados@aperture.labs" | ||
| fill_in "Phone number", with: "2134567eee" | ||
|
|
||
| click_on "Send me reset password instructions" | ||
| expect(page).to have_content "1 error prohibited this User from being saved:" | ||
| expect(page).to have_text("Phone number must be 12 digits including country code (+1)") | ||
| end | ||
|
|
||
| it "displays error if user tries to submit empty form" do | ||
| click_on "Send me reset password instructions" | ||
| expect(page).to have_text("Please enter at least one field.") | ||
| end | ||
|
|
||
| it "redirects to sign up page for email" do | ||
| create(:user, email: "glados@aperture.labs") | ||
| fill_in "Email", with: "glados@aperture.labs" | ||
|
|
||
| click_on "Send me reset password instructions" | ||
| expect(page).to have_content "You will receive an email or SMS with instructions on how to reset your password in a few minutes." | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| require "rails_helper" | ||
|
|
||
| RSpec.describe "users/password/new", type: :view do | ||
| it "displays title" do | ||
| render template: "devise/passwords/new" | ||
| expect(rendered).to have_text("Forgot your password?") | ||
| end | ||
|
|
||
| it "displays text above form fields" do | ||
| render template: "devise/passwords/new" | ||
| expect(rendered).to have_text("Please enter email or phone number to recieve reset instructions.") | ||
| end | ||
|
|
||
| it "displays contact fields for user to reset password" do | ||
| render template: "devise/passwords/new" | ||
| expect(rendered).to have_text("Email") | ||
| expect(rendered).to have_field("user_email") | ||
| expect(rendered).to have_text("Phone number") | ||
| expect(rendered).to have_field("user_phone_number") | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eek