My Rails 5 App only permits an admin or support user to create a user, when the user is created a password is generated and emailed to the user, on the first user login the app forces them to change the password.
I have a password_updated field in my schema that I want to be filled to true when the password is updated, however I am hitting a wall here, not sure if its coder eye and I just cant see what where im going wrong.
my application controller:
# Force User To Change Password On First Login
def after_sign_in_path_for(resource)
if current_user.password_updated == "false"
edit_passwords_path
else
authenticated_root_path
end
end
I have it set up so that if the user tries to skip or jump past the password change they are redirected to the password change.
my passwords controller:
class PasswordsController < ApplicationController
def edit
#user = current_user
end
def update
if current_user.update_with_password(user_params)
current_user.password_updated = "true"
flash[:notice] = 'Your Password Has Been Sucessfully Updated.'
redirect_to authenticated_root_path
else
flash[:error] = 'Oh No! Something Went Wrong, Please Try Again.'
render :edit
end
end
private
def user_params
params.require(:user).permit(:password_updated, :current_password, :password, :password_confirmation)
end
end
Originally I had the application controller looking at the sign in count, however if the user closed out and waited long enough they could log back in and not have to change the password. I felt this was more of a risk.
Any assistance here would be greatly appreciated.
I think the problem is that you don't save current_user after setting password_updated to true.
So the code should something like
def update
update_params = user_params.merge(password_updated: true)
if current_user.update_with_password(update_params)
flash[:notice] = 'Your Password Has Been Successfully Updated.'
redirect_to authenticated_root_path
else
flash[:error] = 'Oh No! Something Went Wrong, Please Try Again.'
render :edit
end
end
This way you would save current_user just once.
I suppose that password_updated is boolean field in DB.
Then in your application_controller.rb you can check it like current_user.password_updated?.
I would suggest to allow admin create a user without a password. You will have to override the password_required method from devise.
def password_required?
new_record? ? false : true
end
For a new record, when admin creates it, password is not required, but when the user signs up, it will prompt to add a password.
Or you can even keep condition like when the user is an admin, return false, else return true.
I would choose a different approach using invitations.
Invited users are created with a token (a crypographically random string) which is used to identify the user. This removes the need to communicate the temporary password in clear-text and you can for example add expiry times to the invitation token for security.
So the app flow is the following:
An admin visits /invitiations/new
He fills in the form with the new users email and POSTs to /invitations
An email is sent to the new user containing a link with an access token.
The new user clicks the link and is sent to /invitations/edit?invitation_token=ABCD12
The user fills in the form with a password, and sends a PATCH to /invitations with an invitation token in the request body.
The user should then be prompted to sign with their new password.
A minimal example is:
class AddInvitationTokenToUser < ActiveRecord::Migration[5.0]
def change
add_column :users, :invitation_token, :string
# this may not work on DBs that do not allow NULL on unique columns
add_index :users, :invitation_token, unique: true
end
end
Then we need to setup the User model to create random invitation tokens.
require 'securerandom'
class User < ApplicationRecord
# #todo skip password validation if user has invitation_token
def set_invitation_token!
self.invitation_token = generate_invitation_token
end
private
def generate_invitation_token
# this ensures that the token is unique
begin
token = SecureRandom.urlsafe_base64
end while User.where(invitation_token: token).any?
token
end
end
Setup a controller for invitations:
class InvitationsController < ApplicationController
before_action :authenticate!, only: [:new, :create]
before_action :authorize!, only: [:new, :create]
prepend_before_action :authenticate_user_from_token!, only: [:edit, :update]
skip_before_action :authenticate!, :authorize!, only: [:edit, :update]
def new
#user = User.new
end
def create
#user = User.new(create_params) do |u|
u.set_invitation_token!
end
if #user.save
# #todo email user invitation email
redirect_to '/somewhere'
else
render :new
end
end
def edit
end
def update
if #user.update(update_params)
#user.update_attibute(:invitation_token, nil)
redirect_to new_session_path, notice: 'Please sign in.'
else
render :edit
end
end
private
def authenticate_user_from_token!
unless params[:invitation_token].present?
raise ActiveRecord::RecordNotFound and return
end
#user = User.find_by!(invitation_token: params[:invitation_token])
end
def create_params
require(:user).permit(:email)
end
def update_params
require(:user).permit(:password, :password_confirmation)
end
end
There are several steps omitted here for brevity like skipping the password validation.
I would encourage you to check out DeviseInvitable for a more complete example of this pattern.
Related
This is my code for changing a password:
class RegistrationsController < Devise::RegistrationsController
protect_from_forgery
def update_password
if current_user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
sign_in(current_user, bypass: true)
redirect_to settings_path, notice: "updated"
else
redirect_to settings_path, alert: current_user.errors.full_messages
end
end
protected
def update_resource(resource, params)
resource.update_without_password(params)
end
def after_sign_up_path_for(_resource)
end
def after_update_path_for(_resource)
settings_path
end
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name])
end
private
def user_params
# NOTE: Using `strong_parameters` gem
params.require(:user).permit(:current_password, :password, :password_confirmation)
end
end
The code works if I enter my current password and set a new one, but it doesn't when I enter a correct current password and as a new password + confirmation I leave empty fields (empty string).
The password will not get changed as "no password", but I get a flash message "updated". How do prevent that? I can think of this:
if current_user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
if params[:user][:password].blank?
redirect_to settings_path, alert: "blank pswd"
return
end
sign_in(current_user, bypass: true)
redirect_to settings_path, notice: "Your password has been updated!"
else
redirect_to settings_path, alert: current_user.errors.full_messages
end
However, this solution is a bit... ugly. Is there a more elegant way to handle this situation?
Thank you in advance
Reading your question I was a bit surprised about this behavior. But looking at the source code I could confirm that this behavior is expected and describe in a comment in the code:
# This method also rejects the password field if it is blank (allowing
# users to change relevant information like the e-mail without changing
# their password). In case the password field is rejected, the confirmation
# is also rejected as long as it is also blank.
I have mixed feelings about how to handle it. I would probably not show an error message because when the user didn't enter a new password they probably would not expect the password to change.
But another way to handle the error message could be to not handle that case in the method at all but to use a before_action:
before_action :ensure_new_password_provided, only: :update_password
private
def ensure_new_password_provided
return if params[:user][:password].present?
redirect_to settings_path, alert: "blank pswd"
end
I am currently building a simple web app with Ruby on Rails that allows logged in users to perform CRUD actions to the User model. I would like to add a function where:
Users can select which actions they can perform per controller;
Ex: User A can perform actions a&b in controller A, whereas User B can only perform action B in controller A. These will be editable via the view.
Only authorized users will have access to editing authorization rights of other users. For example, if User A is authorized, then it can change what User B will be able to do, but User B, who is unauthorized, will not be able to change its own, or anyone's performable actions.
I already have my users controller set up with views and a model
class UsersController < ApplicationController
skip_before_action :already_logged_in?
skip_before_action :not_authorized, only: [:index, :show]
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to users_path
else
render :new
end
end
def show
set_user
end
def edit
set_user
end
def update
if set_user.update(user_params)
redirect_to user_path(set_user)
else
render :edit
end
end
def destroy
if current_user.id == set_user.id
set_user.destroy
session[:user_id] = nil
redirect_to root_path
else
set_user.destroy
redirect_to users_path
end
end
private
def user_params
params.require(:user).permit(:email, :password)
end
def set_user
#user = User.find(params[:id])
end
end
My sessions controller:
class SessionsController < ApplicationController
skip_before_action :login?, except: [:destroy]
skip_before_action :already_logged_in?, only: [:destroy]
skip_before_action :not_authorized
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to user_path(user.id), notice: 'You are now successfully logged in.'
else
flash.now[:alert] = 'Email or Password is Invalid'
render :new
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: 'You have successfully logged out'
end
end
The login/logout function works, no problem there.
I started off by implementing a not_authorized method in the main application controller which by default prevents users from accessing the respective actions if the user role is not equal to 1.
def not_authorized
return if current_user.nil?
redirect_to users_path, notice: 'Not Authorized' unless current_user.role == 1
end
the problem is that I would like to make this editable. So users with role = 1 are able to edit each user's access authorization, if that makes sense.
How would I go about developing this further? I also do not want to use gems, as the sole purpose of this is for me to learn.
Any insights are appreciated. Thank you!
The basics of an authorization system is an exception class:
# app/errors/authorization_error.rb
class AuthorizationError < StandardError; end
And a rescue which will catch when your application raises the error:
class ApplicationController < ActionController::Base
rescue_from 'AuthorizationError', with: :deny_access
private
def deny_access
# see https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses
redirect_to '/somewhere', status: :forbidden
end
end
This avoids repeating the logic all over your controllers while you can still override the deny_access method in subclasses to customize it.
You would then perform authorization checks in your controllers:
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
def create
#thing = current_user.things.new(thing_params)
if #thing.save
redirect_to :thing
else
render :new
end
end
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless #thing.user == current_user || current_user.admin?
end
end
In this pretty typical scenario anybody can create a Thing, but the users can only edit things they have created unless they are admins. "Inlining" everything like this into your controllers can quickly become an unwieldy mess through as the level of complexity grows - which is why gems such as Pundit and CanCanCan extract this out into a separate layer.
Creating a system where the permissions are editable by users of the application is several degrees of magnitude harder to both conceptualize and implement and is really beyond what you should be attempting if you are new to authorization (or Rails). You would need to create a separate table to hold the permissions:
class User < ApplicationRecord
has_many :privileges
end
class Privilege < ApplicationRecord
belongs_to :thing
belongs_to :user
end
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless owner? || admin? || privileged?
end
def owner?
#thing.user == current_user
end
def admin?
current_user.admin?
end
def privileged?
current_user.privileges.where(
thing: #thing,
name: params[:action]
)
end
end
This is really a rudimentary Role-based access control system (RBAC).
I'm building an events app with users who will each have a personal profile. I've set up a few users for the site but when I try and create and/or edit a profile for each user it refers me back to a flash message "That profile doesn't belong to you!" which is in reference to my first user profile which was set up and works fine.
I'm using Devise gem for initial set up but have built out from their with my own user controller. Here's the code from that controller -
class UsersController < ApplicationController
before_action :authenticate_user!
before_action :set_user
before_action :owned_profile, only: [:edit, :update]
def new
#user = User.new
end
def show
#user = User.find(params[:id])
end
def create
end
def edit
#user = current_user #User.find_by(params[:id])
end
def update
#user = User.find_by(params[:id])
if #user.update(user_params)
redirect_to user_path, notice: "Profile successfully updated!"
else
render 'edit'
end
end
private
def user_params
params.require(:user).
permit(:name, :username, :biography, :email, :url)
end
def owned_profile
unless current_user == #user
flash[:alert] = "That profile doesn't belong to you!"
redirect_to root_path
end
end
def set_user
#user = User.find_by(params[:id])
end
end
Any assistance would be appreciated.
I would create an admin. An easy way to do this is to add a column to your users table called admin and make it a boolean. Migrate the db.
Then check to whether a user is an admin before running the owned_profile method. In that method, change: unless current_user == #user to
unless current_user == #user || current_user.admin
Then set yourself as an admin in the console, save and then freely add profiles without that callback running.
If the issue is that Users are not able to edit their own profile, then I believe it is caused by the use of find_by within set_user:
#user = User.find_by(params[:id])
Should be:
#user = User.find(params[:id])
If you truly wanted to use find_by you could do:
#user = User.find_by_id(params[:id])
Or
#user = User.find_by(id: params[:id])
Find_by used as the 2 examples above will not throw an error if a User is not found, while find will.
Sidenote: You can remove the #user assignment within the show action.
You can do it by this way.
When user signing up, automatically creates profile. Good point of this ID of user and profile tables will be the same.
rails g model profile first_name last_name email
rails g migration add_user_id_to_profiles user_id:integer
Profile.rb
belongs_to :user
User.rb
has_one :profile, dependent: :destroy
before_create :set_profile
def set_profile
build_profile(id: self.id, user_id: self.id, email: self.email)
end
GoodLuck.
I want to allow Users to have two edit pages, one which is the default Devise page that requires a password and the other edit page which allows them to edit some parts of their profile without a password.
I have seen their guide pages like this which allows them to edit their profile without providing a password. However I want to have both options available to the user instead of just one.
How would I go about doing this?
My Attempt
I have tried to create my own update route in the Users controller which will solve the problem but this creates a problem when a User resets their password as it gets routed to the User#update which will cause an error as there is no User available during the password reset in Devise.
class UsersController < ApplicationController
before_action :authenticate_user!, :only => [:crop] #:edit , :update
def show
#user = User.find(params[:id])
authorize #user
end
def update
#user = User.find(params[:id])
authorize #user
if #user.update(user_params)
flash[:success] = "You have successfully updated your profile!"
redirect_to user_path(#user)
else
render :edit
end
end
def crop
#user = User.find(params[:id])
authorize #user
end
def index
end
private
def user_params
params.require(:user).permit(:poster_image_crop_x, :poster_image_crop_y, :poster_image_crop_w, :poster_image_crop_h)
end
end
Routes
Rails.application.routes.draw do
resources :users,only: [:show] do
member do
get :crop
end
end
devise_for :users, :controllers => { :omniauth_callbacks => "callbacks",:registrations => :registrations,:passwords => "passwords" }
as :user do
get "/login" => "devise/sessions#new"
get "/register" => "devise/registrations#new"
get "/edit" => "devise/registrations#edit"
delete "/logout" => "devise/sessions#destroy"
end
The code by Devise is suggesting to create your own controller. They probably always require password to be passed if it comes from an action in the UsersController. So you should create a seperate controller, let's call it ProfilesController, this controller is like your normal controller although it does not update a Profile model, but the User model directly... nothing special actually, just check authorization and let the User update any field you'd like directly on the User mode, do not forget to authorize the fields you'd wish to let the user update:
class ProfilesController < ApplicationController
def index
end
....... more code
def update
#user = User.find(params[:id])
authorize #user
if #user.update(user_params)
flash[:success] = "You have successfully updated your profile!"
redirect_to user_path(#user)
else
render :edit
end
end
....... more code
private
def user_params
params.require(:user).permit(:poster_image_crop_x, :poster_image_crop_y, :poster_image_crop_w, :poster_image_crop_h)
end
end
And add resources :profiles to your routes file.
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