As an example, lets say I'm creating a twitter(-ish) clone.
A User has_many Tweets
A Tweet belongs to a User
Users can create tweets
Users can edit and delete their own tweets
Users who are admins can edit and delete all tweets
Users who are admins can edit and delete users who aren't admins
Here's my tweets controller:
class TweetsController < ActionController
before_action :set_tweet, only: [:edit, :update, :destroy]
before_action :only_admins_and_owner, only: [:edit, :update, :destroy]
def edit
# edits the tweet
end
def update
# updates the tweet
end
def destroy
# destroys the tweet
end
private
### security
def only_admins_and_owner
redirect_to root_url unless current_user.is_admin? || current_user === #tweet.user
end
###
def set_tweet
#tweet = Tweet.find(params[:id])
end
end
My users controller:
class UsersController < ActionController
before_action :set_user, only: [:edit, :update, :destroy]
before_action :only_admins_and_user, only: [:edit, :update, :destroy]
def edit
# edits the user
end
def update
# updates the user
end
def destroy
# destroys the user
end
private
### security
def only_admins_and_user
redirect_to root_url unless current_user.is_admin? || current_user === #user
end
###
def set_user
#user = User.find(params[:id])
end
end
And here's my panels controller. Not sure if this is the right way to do it to be honest. What do you think of my naming conventions? I haven't called it AdminController because by panels (plural) I am refering to the multiple panels in the admin area, the user panel (for displaying all of the users and offering administration controls in the view) and the tweets panel:
class PanelsController < ActionController
before_action :only_admins
def users
#users = User.all
end
def tweets
#tweets = Tweet.all
end
private
### security
def only_admins
redirect_to root_url unless current_user.is_admin?
end
###
end
If you think this controller setup is okay, how should I configure my routes to use these actions?
My routes file:
MyApp::Application.routes.draw do
resources :users, except: [:index] do
resources :tweets, except: [:index]
end
end
Now the above works okay if I am a normal user CRUDing away at user and tweets but how should I namespace my Panels controller?
When an admin is viewing users in the panels controller's users view, I want the URLs to look like this:
/control_panel/users
/control_panel/tweets
and when editing in the admin area:
/control_panel/users/12/edit
/control_panel/users/12/tweets/142/edit
but when a user is editing their own user or tweet:
/users/12/edit
/users/12/tweets/142/edit
This is because the panels administration views are vastly different to the user views, but the functionality of editing, updating and deleting is identical so I want to use the already existing actions. Am I doing it right? Not sure how else I could do it, other than adding loads of actions to to the panels controller, def_user_update and def_tweet_update and so on for every single resource. Doesn't feel very nice...
So how should I configure my routes?
I guess I want to sort of create an optional namespace around my two nested routes...
Maybe a concern? If I do that, though, I get an uninitialized constant Panel error.
If you want to use the same controller, which is something I don't really recommend, you can use a scope.
resources :users, except: [:index] do
resources :tweets, except: [:index]
end
scope :control_panel do
resources :users, expect: [:index] do
resources :tweets, except: [:index]
end
end
This gets you all the routes that you want, and they all point to the UsersController and TweetsController
What I recommend instead is using a different controller for admins. You can achieve this with namespaces.
namespace :control_panel do
resources :users, etc: ...
end
You then keep site user's concerns in TweetsController and control panel concerns in ControlPanel::TweetsController in app/controllers/control_panel/tweets_controller.rb
You can read more about namespaces and routing here.
Related
I have the following under my routes.rb:
resources :users do
resources :submitted_terms, only: [:index, :create, :show]
end
I only want the current_user (the logged in user) to be able to see their own submitted_terms in terms of the index and show views. They shouldn't be able to see anybody else's index and show views and other people shouldn't be able to see theirs.
I think I know how to implement this but it feels sort of messy to me. Any thoughts?
You can before_action filter.
before_action :correct_user, only: [#action for which you need this filer]
def correct_user
#submitted_term = SubmittedTerm.find(params[:id])
unless #submitted_term.user == current_user
flash[:notice] = "Insuffient privlage"
redirect_to #some path or render
end
end
you may need to change code or create new action(filter) for checking if user is logged in or not
I blocked display links on the show page:
<% if #post.user == current_user %>
links
<%end%>
but I can't block url adress for unprivileged users:
http://localhost:3000/posts/1/edit
What can I do?
It's good possibility to use Pundit gem (https://github.com/elabs/pundit).
Your policy will look:
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
#user = user
#post = post
end
def edit?
post.user == user
end
end
And your controller's action:
def edit
#post = Post.find_by(id: params[:id])
authorize #post
...
end
What you're looking for is something called authorization
Authentication = finding out if a user is present
Authorization =
determining if they are able to perform specific requests
The answer by Sergei Stralenia is correct - you'll need to use one of the authorization gems -- Pundit and CanCanCan being two of the most popular -- to validate whether a user is able to edit a particular object.
In regard the routing, you'll not be able to remove the edit route, unless you separate it out into something like an admin namespace (I'll explain more in a second).
--
Sergei Stralenia's post showed you how to use Pundit, I'll show you CanCanCan:
#app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, Post
else
can :read, Post
end
end
end
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def edit
#article = Post.find params[:id]
authorize! :edit, #article
end
end
Admin
If you wanted to make a post only editable in an "admin" area, you'd be best using something like the following:
#config/routes.rb
resources :posts, only: [:index, :show]
namespace :admin do
resources :posts, only: [:new, :create, :edit, :update, :destroy]
end
This way, you will literally have no way for a non-admin user to edit/update posts in the front-end. Instead, they'll have to go into the admin area and make it so that they are able to edit it in there...
#app/controllers/admin/posts_controller.rb
class Admin::PostsController < ApplicationController
#actions & authorization in here
end
Within the edit action on your controller, perform the same check - something like:
#post = Post.find_by( id: params[:id] )
unless #post.user == current_user
fail(ActionController::RoutingError, 'User cannot edit this post.')
end
You can simplify the error check into:
fail(ActionController::RoutingError, 'User cannot edit this post.') unless #post.user == current_user
I hope this helps!
I guess the best way to do this is to use before_filter in your posts controller, i.e.:
before_action :authorize_admin, only: [:show, :edit, :update, :destroy]
or:
before_filter :authorize_admin, except: [:show]
where :authorize_admin is the method that You have to define either in posts controller (to use for posts only) or in application controller (to use in all controllers), like this:
def authorize_admin
redirect_to :new_user_session unless current_user&¤t_user.admin?
end
Consider the example in the note for this guide on routing and singular resources Both of these would be directed to 'photos#index', but are different contexts.
/users/1/photos (might list a user's photos)
/photos (list all users' photos)
I want to give the user different options depending on which route was followed to access.
There are two ways to assign this issue.
1st way
In the users_controller.rb
before_action :set_user, only: [:photos]
def photos
#photos = #user.photos
render "photos/index"
end
private
#user = User.find(params[:id])
In routes.rb you need to add this route,
resources :users do
get "photos", on: :member
end
2nd way
In photos_controller.rb
before_action :set_user, only: [:photos]
def index
unless #user.nil?
#photos = #user.photots
else
#photos = Photo.all
end
end
private
def set_user
if params[:user_id].present?
#user = User.where(params[:user_id]).first
end
end
Well first of all, I'd make /users/1/photos an action off users controller instead. However, if you really want to do what you say there, you could check for presence of the user_id param on the photos controller index and fashion your finder appropriately.
For example you can validate route match with rspec, something like the following:
expect(:get => "/users/1/photos").to route_to(
:controller => "photos",
:user_id => "1",
)
I have a jobs pages where users can create new jobs edit and destroy their jobs and I want to let user edit or destroy only their post only if are connected else they will returned to the job show page for this i have this code in my job controller
def require_login
#job = current_user.jobs.find_by_slug(params[:id])
redirect_to job_path if #job.nil?
end
before_action :login_required
def login_required
redirect_to new_user_session_path unless user_signed_in?
end
before_action :login_required, :require_login, only: [:edit, :update, :destroy]
the only part where this don't work is when i not connected and try to edit my job it redirect me to the log in form but after login it redirect me to the home page instead of the edit page
First of all, the code block you pasted looks weird, for three reasons:
Indentation is wrong
You have before_action :login_required twice and it's not even the same
From the methods names it's unclear what you want exactly
From what you wrote, you want a user to edit or destroy a job only if they are logged in - otherwise you want to send them to the jobs index page. If that's right, the code in your controller should look like this:
class JobsController < ApplicationController
before_filter :require_login, :only => [:edit, :update, :destroy]
def edit
# your code here
end
def update
# your code here
end
def destroy
# your code here
end
private
def require_login
redirect_to job_path unless user_signed_in?
end
end
I have a Group resource that I'm trying to set up with proper authorizations.
The authorization logic I'm trying to implement is this:
Only group members should be able to view their group.
An admin can view any group, as well as take other actions.
I'm attempting to do this with the following before_filter statements in the group controller:
before_filter :signed_in_user
before_filter :correct_user, only: :show
before_filter :admin_user, only: [:show, :index, :edit, :update, :destroy]
Correct_user works as I have verified that only group members can view their group. However, what I want to happen is for the admin :show clause to override this, so that an admin can view any group. Currently that is not working. I'm guessing I have something wrong here with my filter ordering and options.
Can someone tell me where I've gone wrong?
EDIT
Adding my method code per Amar's request:
private
def correct_user
# User has to be a member to view
#group = Group.find(params[:id])
redirect_to(root_path) if #group.members.find_by_member_id(current_user).nil?
end
def admin_user
redirect_to(root_path) unless current_user.admin?
end
Update the correct_user method or create another method with the following definition, remove show from other filter and add before_filter with new method.
def correct_user
#group = Group.find(params[:id])
redirect_to(root_path) if #group.members.find_by_member_id(current_user).nil? && !current_user.admin?
end