Using Devise, I know how to protect controller actions from non-signed-in users through:
before_filter :authenticate_user!
In order to illustrate what I am trying to achieve, please see an example:
I have the following controller: (a project belongs to a user)
projects_controller.rb
def create
#project = current_user.projects.new(params[:project])
if #project.save
redirect_to #project
else
render :action => 'new'
end
end
What I am looking for is a way that users can interact more with the website before having to sign up/sign in. Something like:
after_validation :authenticate_user!
if the user is not signed in, and redirect him after success (sign up/sign in) to the "project" show page.
Things I thought:
1.) Change the controller in order to accept a project object without user_id, ask for authentication if the user is not signed in, then update attributes with the user_id
I try to do it like this first and it results to a very ugly code. (Moreover authenticate_user! doesn't redirect to the #project which lead to more customization)
2.) Create a wizard with nested_attributes (project form and nested new registration form and session form)
3.) Something better? (a custom method?)
It seems authologic manages this more easily. I'm not sure it is a reason to switch so I would like to have your idea/answer on this. Thanks!
EDIT
references: Peter Ehrlich answer comment
CONTROLLER WITH VALIDATIONS LOGIC
projects_controller.rb
def create
unless current_user
#project = Project.new(params[:project]) # create a project variable used only for testing validation (this variable will change in resume project method just before being saved)
if #project.valid? # test if validations pass
session['new_project'] = params[:project]
redirect_to '/users/sign_up'
else
render :action => 'new'
end
else
#project = current_user.projects.new(params[:project])
if #project.save
redirect_to #project
else
render :action => 'new'
end
end
end
def resume_project
#project = current_user.projects.new(session.delete('new_project')) # changes the #project variable
#project.save
redirect_to #project
end
routes
get "/resume_project", :controller => 'projects', :action => 'resume_project'
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
def after_sign_in_path_for(resource)
return '/resume_project' if session['new_project'].present?
super
end
Something like this should work:
def create
unless current_user
session['new_project'] = params[:project]
redirect_to '/register'
return
end
# and on to normal stuff
# in your devise controller
def after_sign_in_path
return '/resume_project' if session['new_project'].present?
super
end
# back in projects_controller now
def resume_project
#project.create(session.delete('new_project'))
# you know the drill from here
# I'd also put in a check to make an error if the session is not set- in case they reload or some such
Keep in mind that session is a cookie in the browser, and thus has a size limit (4kb). If you're posting images or other media, you'll have to store them temporarily server-side.
Another option would be to create a userless project, and use a similar technique to allow them to claim it as their own. This would be nice if you wanted unclaimed projects displayed to all to be available as a flow.
I haven't tested it out, but it should be possible to store the action the user was going to, I.e. create, with the params hash that was submitted and redirect to it upon successful login. It would then handle the error cases as normal.
Have you tried that?
Related
I have a users_controller.rb. There are too many method including login, register, forgot_password and logout I want to put auth allow these actions in my ruby controller.
I have done $this->Auth->allow in the CakePHP.
$this->Auth->allow('register', 'login', 'forgot_password', 'logout');
But in the ruby this is very hard to put. Please suggest me -
def login
#title = 'Login'
#render layout: 'login'
end
def dashboard
if logged_in?
#title = 'My Dashboard'
#user = User.get_profile(session[:user_id])
#user = User.get_profile(session[:user_id])
#raise #myProfile.inspect
else
redirect_to '/login'
end
end
def my_profile
if logged_in?
#title = 'My Profile'
#user = User.get_profile(session[:user_id])
else
redirect_to '/login'
end
end
def logout
log_out
redirect_to '/login'
end
Each time I am adding if logged_in? ... else ... end in my every action. So
I want to put Auth Allow in ruby like CakePHP code. Please help me.
Those actions should be in separate controllers, there are plenty of resources available to explain this, search for "RESTful Rails".
Once they are in separate controllers you can use a "before" action to prevent unauthorised users from accessing those actions.
It looks like you've created your own authentication system, instead of using a gem, so if you want a method to check for a logged in user, you can add it.
In application_controller.rb
def authenticate_user
redirect_to login_path unless logged_in?
end
Then in any controller you want to require a user to be signed in you can do
class YourController < ApplicationContoller
before_action :authetnicate_user, except: [:actions_that_doesnt_need_auth]
...
# All normal methods
end
That being said - the previous answer about using RESTful resources is important to understand and keep in mind. If you have questions you can ask :)
I have a relationship user ("devise") that has many events.
I want to prevent users from editing events that do not belong to them and stop users from accessing the edit action by entering something like 'http://localhost:3000/events/65/edit' into the browser.
I also want to redirect the user back to the page they were on when clicking on the edit event link.
I tried the following two methods without success:
def edit
if current_user == #event.user_id
#event = Event.find(params[:id])
else
redirect_to events_path
end
def edit
#event = Event.find(params[:id])
unless session[:id] == #event.user_id
redirect_to events_path
return
end
end
If you only need this kind of authorization logic in this controller, something like this would be possible:
class User < ActiveRecord::Base
has_many :events
end
class EventsController < ApplicationController
def edit
#event = current_user.events.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to events_path, notice: "You cannot edit this event."
end
end
The rescue-block is really optional. If you remove it, the user will get a 404 Not found error message if she visits the edit URL for an event she didn't create,
If you expect to use authorization other places in your application, I would advise you to look into CanCan. It's a gem that sentralizes rules for authorization and access in an Ability class.
Try adding a before filter (it can be used for other actions as well if needed):
before_filter :check_user, :only => [:edit]
def check_user
#event = Event.find(params[:id])
unless current_user.id == #event.user_id
redirect_to (request.referrer || root_path)
return
end
end
The idea behind your first method is fine, but that comparison will always fail. current_user is a User object; #event.user_id is an integer (or possibly some form of UUID).
You need to either compare a User object to a User object:
if current_user == #event.user
Or an ID to an ID:
if current_user.id == #event.user_id
When I create a user in my database, it also creates a unique identifier string.
In my routes, I have:
match '/users/:unique_identifer', :to => 'users#show'
This part is working fine. When I go to /users/xyz, it brings me to the show action for the appropriate user.
However, when I try to update the user record, it redirect me back to /users/SOMENUMBER where SOMENUMBER is the user's ID. This causes an error since the show action in the controller has:
def show
#user = User.find_by_unique_identifier(params[:unique_identifer])
end
In other words, the show action is now only looking up the user by their unique identifier and not the user id.
The update action is as follows:
def update
#user = User.find_by_unique_identifier(params[:unique_identifer])
if #user == current_user && #user.update_attributes(params[:user])
redirect_to #user
else
redirect_to #user
end
end
How do I get the update action to redirect to the user's show action but with the appropriate link (/users/unique_identifier) instead of /users/ID?
You should redefine to_param (and from_param to make life easier), it is used to generate links for objects, and by default use id field
so in your case
class User < ActiveRecord::Base
def to_param
unique_identifer
end
end
and now users_path(#user) should give you /users/your-uniq-identifier
here is nice description with examples:
http://apidock.com/rails/v3.1.0/ActiveRecord/Base/to_param
Try this
redirect_to user_path(#user.unique_identifer)
or
redirect_to :action => :show, :unique_identifer => #user.unique_identifer
In Rails3 application i have a number of models with user_id - this way i'm saying: it was created by some user.
Like:
current_user.id #=> 1
#item.user_id #=> 1
# this item created by user with id 1
And i want to restrict current_user's acess to items which was not created by him/her.
Something like:
if #item.user_id == current_user.id
#everything is fine
else
#redirect somwhere with flash "You don't have an access here"
end
What is the best way for this, because i have multiple number of models (and controllers to show/edit/destroy) with such a user_id?
Use CanCan!
With it you will be able to define permissions declaratively, like this:
can :read, Project, :user_id => user.id
And later enforce this rule:
def show
#project = Project.find(params[:id])
authorize! :read, #project
end
authorize! will raise an exception, but you can check in a more peaceful manner:
<%= link_to 'Link to a project', #project if can? :read, #project %>
You can intercept authorization errors and handle them in one place:
class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
end
end
The simplest way to do this, is to use Active Record's has_many.
Namely, in a controller, whenever you load the Item, you just say
#item = current_user.items.find(params[:id])
This way you don't have to do any work to check.
I know I should put the code in the create action of the users controller, but I'm not sure what code I should put. I also assume it should call the create action in my sessions controller, but again I'm not sure how...
By the way I tried render :template => 'sessions/create' in the create action of the users controller, but I get this error when signing up:
Template is missing
Missing template sessions/create with {:locale=>[:en, :en], :formats=>[:html], :handlers=>[:rjs, :rhtml, :erb, :rxml, :builder]} in view paths "/rubyprograms/dreamstill/app/views", "/rubyprograms/dreamstill/vendor/plugins/facebox_render/app/views"
This is all in my application controller:
protected
# Returns the currently logged in user or nil if there isn't one
def current_user
return unless session[:user_id]
#current_user ||= User.find_by_id(session[:user_id])
end
# Make current_user available in templates as a helper
helper_method :current_user
# Filter method to enforce a login requirement
# Apply as a before_filter on any controller you want to protect
def authenticate
logged_in? ? true : access_denied
end
# Predicate method to test for a logged in user
def logged_in?
current_user.is_a? User
end
# Make logged_in? available in templates as a helper
helper_method :logged_in?
def access_denied
respond_to do |format|
format.html do
flash[:alert] = "You must log in to peform this action."
redirect_to root_path
end
format.js do
render_to_facebox(:partial => 'sessions/login_box')
end
end
false
end
Somewhere in your controllers you have something that looks like this:
user = User.new
# set attributes
user.save
render :template => 'sessions/create' # Probably based on your question
All you need to do is update the session to:
user = User.new
# set attributes
if(user.save)
session[:user_id] = user.id
# Send them somewhere useful
else
# Handle the error
end
They're signed in once session[:user_id] is set.
Technically?
In your controller, after you create your user, this code:
#current_user = user
should get you going (looks like you're using restful_authentication).
Now, whether it's a good idea to log in a user automatically without verifying their email address / whatever else is up for debate.
You seem that you just begin with Rails right ? I would highly recommend that you use a gem like Devise to handle your user registrations.
However, if you insist on doing it manually, you would just need to create a session variable that verifies whether a user is logged in or not. Then, you can add a helper like current_user, to get the user if user session shows he/she is logged in.
I see that you have a sessions controller there. Are you trying to use restful_authentication ? If so, once more i highly recommend switching to Devise :)
OLD CODE USING RESTFUL AUTHENTICATION - SESSIONS CONTROLLER
# This controller handles the login/logout function of the site.
class SessionsController < ApplicationController
# Be sure to include AuthenticationSystem in Application Controller instead
include AuthenticatedSystem
# render new.erb.html
def new
end
def create
logout_keeping_session!
user = User.authenticate(params[:login], params[:password])
if user
# Protects against session fixation attacks, causes request forgery
# protection if user resubmits an earlier form using back
# button. Uncomment if you understand the tradeoffs.
# reset_session
self.current_user = user
new_cookie_flag = (params[:remember_me] == "1")
handle_remember_cookie! new_cookie_flag
flash[:notice] = "Logged in successfully"
redirect_to :controller=>'Town'
else
note_failed_signin
#login = params[:login]
#remember_me = params[:remember_me]
render :action => 'new'
end
end
def destroy
logout_killing_session!
flash[:notice] = "You have been logged out."
redirect_back_or_default('/')
end
protected
# Track failed login attempts
def note_failed_signin
flash[:error] = "Couldn't log you in as '#{params[:login]}'"
logger.warn "Failed login for '#{params[:login]}' from #{request.remote_ip} at #{Time.now.utc}"
end
end