I am trying to refactor the update action in my Rails action, so that users can change their own email address only after confirming it by clicking on a link that I send to them.
class UsersController < ApplicationController
before_filter :authorized_user
def update
current_email = #user.email
new_email = params[:user][:email].downcase.to_s
if #user.update_attributes(params[:user])
if new_email != current_email
#user.change_email(current_email, new_email)
flash[:success] = "Please click on the link that we've sent you."
else
flash[:success] = "User updated."
end
redirect_to edit_user_path(#user)
else
render :edit
end
end
def confirm_email
#user = User.find_by_email_token!(params[:id])
#user.email = #user.new_email
#user.save
end
private
def authorized_user
#user = User.find(params[:id])
redirect_to(root_path) unless current_user?(#user)
end
end
This function saves the new email to a database field new_email. email will be replaced only after the user has confirmed his new_email through a URL:
class User < ActiveRecord::Base
def change_email(old_email, new_email)
self.email = old_email
self.new_email = new_email.downcase
self.send_email_confirmation_link
end
end
The code partially works, but I wonder if there's a more elegant way to do this, maybe by using an after_save callback or at least moving more code to the model.
What would be the best way to do this?
Thanks for any help!
P.S. Please don't suggest to use Devise for this. I really want to build my own authentication system here :-)
I would advise you against using ActiveRecord callbacks to perform business logic: ActiveRecord models should only be a thin wrapper around the database persistence layer.
Look at how the controller code can be changed:
def update
if UpdatesUserCheckingEmail.new(#user, params[:user], flash).execute!
redirect_to edit_user_path(#user)
else
render :edit
end
end
All the business logic is performed by an external object, which encapsulates all your business logic (which you can put in app/services/updates_user_checking_email.rb)
class UpdatesUserCheckingEmail
attr_reader :user, :user_params, :flash
def initialize(user, user_params, options = {})
#user = user
#user_params = user_params
#flash = options[:flash]
end
def execute!
if user.update_attributes(user_params)
if new_email != current_email
user.change_email(current_email, new_email)
flash[:success] = "Please click on the link that we've sent you."
else
flash[:success] = "User updated."
end
end
end
private
def current_email
user.email
end
def new_email
user_params[:email].downcase.to_s
end
end
I'd also advise you to move the logic which sends the email out of the ActiveRecord model and inside a dedicated service object. This will make your app much more easier to change (and to test) in the future!
You can find a lot more about these concepts here: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
I think you should not check current_email and new_email after update in database because it should before the database update. Another one is you are sending link to user after updating email to the database. So, that could not meet your goal i.e. "email will be replaced only after the user has confirmed his new_email through a URL." You should create new action for updating user email or you should write logic for updating user email when user get email of "reset email" in update action of UserController. Following is something simple approach to resolve your problem:
class UsersController < ApplicationController
def send_email_rest
#user.change_email(#user.email, params[:new_email]) if params[:new_email].present?
end
def update
if #user.update_attributes(params[:user])
#stuff you want to do
end
end
end
Hope that helps!!!
Related
#app/controllers/sessions_controller.rb
class SessionController < ApplicationController
def new
#session = Session.new
end
def fetch
##user = User.session(params [:user])
redirect_to "http://www.google.com"
end
def create
emai = params[:email]
puts emai
user = User.find_by(:email => session[:emai])
#user = User.find_by (params [:email])
#user = User.find_by email: 'abc#xyz.com'
#user = User.find_by(params[:Email])
#if (session[:Email] = user.email)
if (user)
redirect_to "http://www.yahoo.com"
flash[:notice] = "You signed up successfully"
flash[:color]= "valid"
else
flash[:notice] = "Form is invalid"
flash[:color]= "invalid"
redirect_to "http://www.google.com"
end
#redirect_to "http://www.yahoo.com"
end
end
every time i execute my view i get redirected to google.com even though i pass the parameters.
Edit by R Peck:
My logic should send people to Yahoo if the params are set, but still sends to Google, how can I fix this?
Try:
user = User.find_by(:email => params[:sessions][:emai])
You are not getting the value of email if you only call params[:email] you should call parent first before calling the child params[:sessions][:email].
Several things wrong with your code.
Here's what I'd write:
#app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
#session = Session.new
end
def create
email = params[:sessions][:email]
user = User.find_by email: email
url = user ? "google" : "yahoo"
colour = user ? "valid" : "invalid"
notice = user ? "You signed up successfully" : "Your form is invalid"
redirect_to "http://#{url}.com", notice: notice, color: colour
end
private
def session_params
params.require(:session).permit(:session, :params)
end
end
OOP
I think this may be a little advanced but I'll write it anyway, for my own benefit.
Rails is object orientated (it's built on Ruby which is an OOP language). This means that each time you create/call a controller, it should be centered around objects.
A good example for you would be the Devise controllers.
This has a sessions_controller which essentially allows you to CRUD (Create Read Update Destroy) a session. This is the correct way to use a controller.
Your implementation seems to be dealing with a user, rather than a session, and as such you'd be best using a users_controller to fix it:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new
#user.save
end
end
Having said that, it does seem that you're probably going to resolve the issue to make it so that you can use the User to build a new session.
I guess it's best to remember that you have to ensure you're able to appreciate a good structure for your application
I have an auth system from scratch, and when a user clicks on 'edit profile' it has to input the current password no matter the field he wants to edit.
def update
if params[:user][:password].present?
authenticated = #user.authenticate(params[:user][:current_password])
if authenticated && #user.update(user_params)
redirect_to root_url
flash[:notice] = "Your profile was successfully updated!"
else
#user.errors.add(:current_password, 'is invalid') unless authenticated
render :edit
end
elsif #user.update(user_params)
redirect_to root_url
flash[:notice] = "Your profile was successfully updated!"
else
render :edit
end
end
How can I call authenticate or use some context model validation only for the scenario when the user wants to change his password?
I wouldn't recommend mixing this logic into the model because you end up with complexity that is hard to follow as your application grows over time.
Try taking a look into form objects:
Form-backing objects for fun and profit
Railscast #416 Form Objects [paid subscription required]
I'd implement something like this:
class UserUpdateForm
include ActiveModel::Model
# Attributes
attr_accessor :user, :new_password, :new_password_confirmation
# Validations
validates :current_password, if: :new_password
validate :authenticate, if: :current_password
validates :new_password, confirmation: true, allow_blank: true
def initialize(user)
self.user = user
end
def submit(params)
self.new_password = params[:new_password]
self.new_password_confirmation = params[:new_password_confirmation]
if self.valid?
# Set other attributes as needed, then set new password below.
self.user.password = self.new_password if self.new_password.present?
self.user.save
else
false
end
end
private
def authenticate
unless self.authenticate(self.current_password)
self.errors.add(:current_password, 'is invalid')
end
end
end
Then you can call it from your controller like so:
def update
#user_update_form = UserUpdateForm.new(#user)
if #user_update_form.submit(params)
flash[:notice] = "Your profile was successfully updated!"
redirect_to root_url
else
render :edit
end
end
See the links above for how to handle the view and such. This is just to get you started.
You may create a nested if-else in this action statement that will check for existence of new_password and new_password_confirmation (or whatever the new password and confirmation fields are called) in the params[:user] object. If they are present - you may redirect to some king of page with request to enter existent password.
Another way is to use ajax to show asynchronously the dialog box with the same request (like respond_with self-invoking javascript function that handles that). Then handle submit button in of the dialog in the other action of the controller.
Update (considering use of validators):
Considering validation you may write your own validator (for password) and condition to check when the new password field come with some data from the client.
I think it could look like this:
validate :password_update?
def password_update?
if new_password.present?
if current_password !== self.password
errors.add(:current_password, "must be supplied!")
else
# update data and password
end
else
# do your regular update
end
end
So after the user signs up, i redirect them to my additional info page where i collect some more information. However, something is wrong with my design/implementation as rails is saying im missing users/create template
this is my users controller
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def additional_info
#user = User.new(user_addinfo)
if #user.save
redirect_to show_path
end
end
def create
#user = User.new(user_params)
if #user.save
# UserMailer.welcome_email(#user).deliver
sign_in #user
redirect_to additional_info_path
flash[:success] = "Welcome to InYourShoes!"
#return #user
else
render'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def user_addinfo
params.permit(:year)
end
end
def show is the user profile page i want to show after redirecting to the additional_info page
def additional_info is just take additional info from the private method def user_addinfo
def create is the sign up process.
After entering the basic user info, it gets redirected to additional which is fine. but after the additional, it says im missing the users/create template, but my code i attempted to redirect to show_path and #usersshow, still doesnt work
any suggestions? sorry if this seems intuitive but Im new to rails.
I think your problem is in the additional_info method, as i said in the comment. What you're doing is:
creating a user
creating a session for the user (sign_in #user) - storing somewhere the user_id in the session
redirecting to your additional_info page
And here comes the problem. As the user is already signed in you don't have any need to create a new user with additional params. You should have some helper to retrieve the current signed in user (like current_user) and in additional_info method, just update it.
So your additional_info method would become something like:
def additional_info
user = User.find session[:user_id]
user.update params[:user]
redirect_to user_path #show action
end
I am new to Rails. Particularly in dealing with the vagaries between Rails 3 and 4. I have been learning from RailsCast and MHartl's tutorial.
I successfully got the code in RailsCast #274 to work by using the answer in the question linked below:
ActiveModel::ForbiddenAttributesError in PasswordResetsController#update
My concern is that this fix will leave me vulnerable to issues in the future, be it security or otherwise. If there is a "right" way to do this I would like to know. Here is my code block:
class PasswordResetsController < ApplicationController
def create
user = User.find_by_email(params[:email])
user.send_password_reset if user
redirect_to root_url, :notice => "Email sent with password reset instructions."
end
def edit
#user = User.find_by_password_reset_token!(params[:id])
end
def update
#user = User.find_by_password_reset_token!(params[:id])
if #user.password_reset_sent_at < 2.hours.ago
redirect_to new_password_reset_path, :alert => "Password reset has expired."
elsif #user.update_attributes(params.require(:user).permit(:password, :password_confirmation))
redirect_to root_url, :notice => "Password has been reset."
else
render :edit
end
end
end
you need to setup your params first. define a private method inside your class
private
def model_params
params.require(:model).permit(:list :all :your :attributes)
end
then when you do an update, use something like:
#model.update(model_params)
mass assignment is a cool thing in rails, but you need to make sure you are protected
hope that helps
I use devise_invitable with Rails and need some help. I want make user logged in after accept invitation. Here is my InvitationsController
class InvitationsController < Devise::InvitationsController
def update
if User.accept_invitation!(user_params)
# log in user here
redirect_to dashboard_show_path, notice: t('invitaion.accepted')
else
redirect_to root_path, error: t('invitation.not_accepted')
end
end
private
def user_params
params.require(:user).permit(:invitation_token, :password, :password_confirmation)
end
end
You can see comment in code
# log in user here
here I want log in the user who has accept the invitation.
Thanks.
The method your looking for is sign_in, try this:
def update
if User.accept_invitation!(user_params)
sign_in(params[:user])
redirect_to dashboard_show_path, notice: t('invitaion.accepted')
else
redirect_to root_path, error: t('invitation.not_accepted')
end
end
However I should note that devise_invitable, by default, signs in users after they have accepted an invitation. See the default update action here, if you wish to use the default functionality simply call the super method or don't implement the update action at all.
Here is a minor update for the Noz version:
# invitations_controller.rb
def update
user = User.accept_invitation!(update_resource_params)
if user
sign_in(user)
redirect_to root_path, notice: t('devise.layout.edit_invitation.accepted')
else
redirect_to root_path
end
end
private
def update_resource_params
params.require(:user).permit(:password, :first_name, :last_name, :invitation_token)
end
This definition update won't throw Could not find a valid mapping for {...} and instead login the user after invitation acceptance.