So I have an app in which the users login with their cell phone numbers and get notifications via text/sms. It's a mobile app. I send texts via the applicationmailer by sending emails to "33333333#vtext.com" etc.
However, I have hit a wall with how to override the password reset instructions. I want the message to be sent via text (i don't have their email address), but how do I override devise to do this? I can have the user enter in their number and then do a lookup (i store the contact path as a field in the user, I generate the string in the backend, they don't have to do it).
Ideas?
thanks a bunch!
You can do this by changing your
passwords_controller:
def create
assign_resource
if #resource
#resource.send_reset_password_instructions_email_sms
errors = #resource.errors
errors.empty? ? head(:no_content) : render_create_error(errors)
else
head(:not_found)
end
end
private
def assign_resource
#email = resource_params[:email]
phone_number = resource_params[:phone_number]
if #email
#resource = find_resource(:email, #email)
elsif phone_number
#resource = find_resource(:phone_number, phone_number)
end
end
def find_resource(field, value)
# overrides devise. To allow reset with other fields
resource_class.where(field => value).first
end
def resource_params
params.permit(:email, :phone_number)
end
and then including this new concern in the users model
module Concerns
module RecoverableCustomized
extend ActiveSupport::Concern
def send_reset_password_instructions_email_sms
raw_token = set_reset_password_token
send_reset_password_instructions_by_email(raw_token) if email
send_reset_password_instructions_by_sms(raw_token) if phone_number
end
private
def send_reset_password_instructions_by_email(raw_token)
send_reset_password_instructions_notification(raw_token)
end
def send_reset_password_instructions_by_sms(raw_token)
TexterResetPasswordJob.perform_later(id, raw_token)
end
end
end
which basically uses the private methods that the devise method sent_reset_password_instructions uses adding your own texting logic.
Related
I am trying to send an welcome email in a rails application after a user signs up. Currently I have a redirection that will take them to a specific page. I have a mailer method that I want to send but it doesn't work. Is there another way to do this?
def after_sign_up_path_for(resource)
if current_user
ModelMailer.new_user_notification(#user).deliver
'/dashboard'
end
end
You could send the email in the user model, with a simple after_create:
class User << ActiveRecord::Base
# ...
after_create: send_welcome_email
def send_welcome_email
ModelMailer.new_user_notification(self).deliver
end
end
I am using devise invitable in my app: a user fills a form with email, that creates a user in my database and sends an invitation to the email address. The other person can then confirm its account by setting up his password.
Everything works fine until the invited user tries to log in by setting up his password. When he submits the form, I get this error:
undefined local variable or method `this' for #
here's my invitation controller:
class Users::InvitationsController < Devise::InvitationsController
def new
#user = User.new
end
def create
#user = User.new
#user.save
end
def update
if this
redirect_to root_path
else
super
end
end
private
# this is called when creating invitation
# should return an instance of resource class
def invite_resource
## skip sending emails on invite
resource_class.invite!(invite_params, current_inviter) do |u|
u.skip_invitation = true
end
end
# this is called when accepting invitation
# should return an instance of resource class
def accept_resource
resource = resource_class.accept_invitation!(update_resource_params)
## Report accepting invitation to analytics
Analytics.report('invite.accept', resource.id)
resource
end
end
What is this "this" thing ?
In my devise sign-up page, i have implemented an ip tracking feature that, when a user signs up, sends the country the user comes from, in order to populate the newly created account's attribute 'user_country'
It works but as a newbie I don't know how to test with rspec that this works i.e that if i create a user who signs up, then AFTER account creation, that user_country is not empty any more.
Here how i use a /app/controllers/registrations_controller.rb method to assign a country to the user's newly created accoun on controllers/registrations_controller.rb- see below
class RegistrationsController < Devise::RegistrationsController
layout 'lightbox'
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
# required for settings form to submit when password is left blank
if account_update_params[:password].blank?
account_update_params.delete("password")
account_update_params.delete("password_confirmation")
end
#user = User.find(current_user.id)
if #user.update(account_update_params) # Rails 4 .update introduced with same effect as .update_attributes
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
render "edit"
end
end
# for Rails 4 Strong Parameters
def resource_params
params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :user_country)
end
private :resource_params
protected
def after_sign_up_path_for(resource)
resource.update(user_country: set_location_by_ip_lookup.country) #use concerns/CountrySetter loaded by ApplicationController
root_path
end
end
If it helps, this is how I find the country of a visitor:
module CountrySetter
extend ActiveSupport::Concern
included do
before_filter :set_location_by_ip_lookup
end
# we use geocoder gem
# output: ip address
def set_location_by_ip_lookup
if Rails.env.development? or Rails.env.test?
Geocoder.search(request.remote_ip).first
else
request.location
end
end
end
you need to use doubles in your test.
describe UserController do
it 'sets the request location with Geocoder' do
geocoder = class_double("Geocoder") # class double
location = double('location', country: 'some country') # basic double with return values
expect(geocoder).to receive(:search).and_return(location) # stubbing return value
post :create, user_params
user = assigns(:user)
expect(user.user_country).to eq('some country')
end
end
this way, we create a fake location and geocoder, and also create fake return values. then we assert that the return value from geocoder gets persisted as a user attribute. this way, we don't actually need to test whether Geocoder is working, and simplifies our test setup a lot.
the concepts used here:
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/basics/test-doubles
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/verifying-doubles/using-a-class-double
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/configuring-responses/returning-a-value
if you want to be more robust, you might be able to fake ip address in the test request context, then assert what arguments the fake geocoder is receiving with argument matching: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/setting-constraints/matching-arguments
in general, you should utilize mocks to isolate your tests' concerns. see https://relishapp.com/rspec/rspec-mocks/v/3-0/docs for more documentation
I am using devise gem. Devise send reset password token in mail when user clicks on forget password link. User follow the link and reset his password by entering new password and confirm new password.
When I follow the same mail link again, it again allow the user to reset password in the same way as above.
Now, I want the reset password token to clear once it is used. So that when your follow the previously used send link from old mail, he must get message that "Invalid token"
How can I do this?
Thanks in advance.
Easier and safer solution than what was proposed:
Create your own passwords controller, I chose to place it under controllers/auth
controllers/auth/passwords_controller.rb
class Auth::PasswordsController < Devise::PasswordsController
def update
super do |resource|
if resource.reset_password_token_changed? and resource.reset_password_token_was.nil?
resource.reset_password_token = nil
end
end
end
end
This fix many problem with papertrail for example, and anyhow save one access to the DB
You can try either of the following methods
# reset_password_within = 1.day and reset_password_sent_at = today
reset_password_period_valid? # returns true
# reset_password_within = 5.days and reset_password_sent_at = 4.days.ago
reset_password_period_valid? # returns true
# reset_password_within = 5.days and reset_password_sent_at = 5.days.ago
reset_password_period_valid? # returns false
# reset_password_within = 0.days
reset_password_period_valid? # will always return false
or you can call the instance methods like clear reset password token or by calling
clear_reset_password_token
or after_password_reset methods.
I think this hack should be more easier if you do the following in your user model or the model that has been used by devise.
class YourModel < ActiveRecord::Base
...
def after_password_reset
self.clear_reset_password_token if not (self.reset_password_token.nil? and self.reset_password_sent_at.nil?)
end
end
I suggest not to use your controller to perform business operation. This after_password_reset password is used to called after clear_reset_password token in devise. Here is the reference:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/recoverable.rb#L39
Hope this will help.
I have achieved above by overriding Devise::PasswordsController in application.
Devise handle reset password on PasswordController#edit action.
On edit, I have checked if the reset password token is valid or not. If its valid I allow user to reset password otherwise redirect user to sign in page with "Password token is invalid message".
For devise 3.0
class Users::PasswordsController < Devise::PasswordsController
def edit
self.resource = resource_class.find_or_initialize_with_error_by(:reset_password_token, params[:reset_password_token])
if !resource.errors.empty?
flash[:alert] = "Password token is invalid"
redirect_to new_session_path(resource_name)
end
end
end
For devise 3.1
class Users::PasswordsController < Devise::PasswordsController
def edit
original_token = params[:reset_password_token]
reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)
self.resource = resource_class.find_or_initialize_with_error_by(:reset_password_token, reset_password_token)
if !resource.errors.empty?
flash[:alert] = "Password token is invalid"
redirect_to new_session_path(resource_name)
end
end
end
For my small Rails application, I am using bcrypt to hash users' passwords when they are stored. However, when loading the new user form, I was hit with "invalid hash" for the password, as my new action was
def new
#user = User.new
end
which does not make a new password, which is thus invalid. To remedy this, I tried using
<%= form_for :user, url: users_path do |f| %>
which does not require a user object, allowing me to make that in the create action. However, error handling still needs the User object and throws a nil error
I feel that there should be a "right" way to do this. Can anyone enlighten me?
My user model is as such:
require 'bcrypt'
class User < ActiveRecord::Base
# For user of user.password_hash. Thanks, bcrypt!
include BCrypt
before_save { self.email = email.downcase }
# Validates uniqueness of email
validates_uniqueness_of :email
# Set relationship to lists
has_many :lists
def make_new_password
new_password = Array.new(10).map { (65 + rand(58)).chr }.join
self.password_hash = Password.create(new_password)
end
def password
#password ||= Password.new(password_hash)
end
def password=(new_password)
#password = Password.create(new_password)
self.password_hash = #password
end
end
I feel like this book can help you find the right way to do user authentication. (sorry that is the best I can do with the information you have provided).
Hope this helps :)