Rails says that "role", a property I made doesn't exist - ruby-on-rails

I clearly made the role for users (as you can see down below) but it says it doesn't exist. Help please? By the way, you can see how I'm hardcoding myself.
app/controllers/application-controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def require_user
redirect_to '/login' unless current_user
end
def require_admin
redirect_to '/' unless current_user.admin
end
end
User.create(first_name: "Johnny", last_name: "Appleseed", email: "j.appleseed#example", password: "MY AWESOME PASSWORD THAT NOBODY KNOWS", role: "admin")
db/migrate/20160109170743_create_users:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
t.string :password_digest
t.string :role, :default => "reader"
t.timestamps null: false
end
end
end
app/controllers/users-controller:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
redirect_to '/'
else
redirect_to '/signup'
end
end
private
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :role)
end
end

Without knowing the specific error message, I can only speculate that your error is here:
def require_admin
redirect_to '/' unless current_user.admin
end
Regardless of the attributes you have in your model / db, you'll only get instance methods you've defined. You don't have admin in your User object, thus making current_user.admin invalid.
You'd need to use the following:
#app/models/user.rb
class User < ActiveRecord::Base
def admin?
self.role == "admin"
end
end
current_user.admin? #-> true / false
Whilst the question mark isn't required, it denotes the evaluation of an object's properties (true / false).
As an aside, you may want to look at adding an enum to your User model:
#app/models/user.rb
class User < ActiveRecord::Base
enum role: [:reader, :admin]
end
This will give you a series of instance & class methods to better help your logic:
#user = User.find params[:id]
if #user.admin?
...
#admins = User.admin
#-> collection of "admin" users
To do it, you'll need to change your role column from string to integer

I would suggest that you use boolean for the column role.
User.rb
def admin?
self.role == true
end
so you can do
redirect_to '/' unless current_user.admin?

Related

How do I prevent model update from changing an attribute?

I have Account model with attribute role. Roles wrote with enum role: [:user, :admin]. I want that if left one account with role admin he can't update his role to user. I think need to write something like this: #account = Account.where(role: params[: role])... and then I don't know.
account.rb
class Account < ApplicationRecord
enum role: [:user, :admin]
end
accounts_controller.rb
class AccountsController < ApplicationController
def index
#accounts = Account.all
end
def update
#account = Account.find(params[:id])
redirect_to accounts_path if account.update(role: params[:role])
end
end
schema.rb
create_table "accounts", force: :cascade do |t|
t.integer "role", default: 0
end
I think what you want is a model callback before_update that acts like a validation.
class Account < ApplicationRecord
enum role: [:user, :admin]
before_update :insure_admin, if: -> { role == :admin && role_changed? }
private
def insure_admin
errors.add(:role, "admin cannot switch to regular user")
end
end
This should prevent the account.update(role: params[:role]) from returning true but you'll probably want to handle the error in your controller, something like:
class AccountsController < ApplicationController
def index
#accounts = Account.all
end
def update
#account = Account.find(params[:id])
if account.update(role: params[:role])
redirect_to accounts_path
else
redirect_to :back, flash: account.errors.full_messages
end
end
end
You might also want to add front end form validation to not allow the form to change role if the account is already persisted.

Validation give me not correct result

I wrote validation which check if role admin only one, role doesn't update on user. It's work, but then I have only one admin I can't update another users to admin. Wrote validation exception.
I try write math operators, like <= and <=> but it not working. I need what I can update user to admin if I have only one admin.
account.rb
class Account < ApplicationRecord
enum role: %i[user admin]
validate :admin_role
private
def admin_role
errors.add(:role, 'Must be one admin') if Account.where(role: :admin).count == 1
end
end
accounts_controller
class AccountsController < ApplicationController
def index
#accounts = Account.all.order(:id)
end
def update
#account = Account.find(params[:id])
if #account.valid?
#account.update(role: params[:role])
else
flash[:danger] = #account.errors.full_messages
end
redirect_to accounts_path
end
end
schema.rb
create_table "accounts", force: :cascade do |t|
t.integer "role", default: 0
end

Why does my regular user have the same permissions as the admin user?

I am using this command in my views/welcome/index.html.erb:
<% if current_user.admin? %>
<%= link_to 'Post New Load Data!', new_article_path %>
<% else %>
<% end %>
Earlier today, this would allow only my admin user to see this path.
I installed Devise a few hours ago but ended up not liking it. So I have gone through and removed what I thought was every file that it created.
Earlier, if I wanted a regular user to see the path, I would use...
<% if current_user %>
instead of
<% if current_user && current_user.admin? %>
I don't if that is what is creating my problem. My migrations did get messed up so I had to reset everything and I created new migration under db/migrate/20161209013349_create_users.rb:
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.string :admin, :boolean, null: false, default: false
t.timestamps
end
end
end
I checked my MySQL users table and the non admin user has a 0 under the admin column. My admin user has as 1 under the admin column. There is a 0 under the boolean column for both regular user and admin.
my application_controller.rb:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
def authorize
redirect_to '/login' unless current_user
end
end
users_controller.rb:
class UsersController < ApplicationController
def new
end
def create
if User.exists?(email: params[:user][:email])
redirect_to '/articles?user_or_pass_already_exists'
else
user = User.new(user_params)
if user.save
session[:user_id] = user.id
redirect_to '/'
else
redirect_to '/signup'
end
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
Any ideas on what caused this?
I think it's the way I set up this model.
EDIT
I also created this method for my admin in /app/models/user.rb:
class User < ApplicationRecord
has_secure_password
# convienience method to access the vaulue of admin:
def admin?
admin
end
# this makes sure the same email and user can't be registered twice
# this only works well if you are only wanting to validate one field such as name
#validates :email, uniqueness: true
end
This is why I am using admin? in my current_user.admin?
First, You must understand that even If you hide links from user You need server side check (authorization) before every action. cancancan gem will help you.
Now, for your question,
def admin?
admin
end
This method will return value of admin column.
According to this line in your migration
t.string :admin, :boolean, null: false, default: false
My guess is column type of admin column in database is string. Where It should be of type boolean. All string values are true in ruby.
2.2.0 :004 > if "0"
2.2.0 :005?> puts "I am true"
2.2.0 :006?> end
(irb):6: warning: string literal in condition
I am true
So either change your column type to boolean or change your method in user.rb like this
def admin?
admin == "1"
end

Rails authorization with cancan

I am following this tutorial
I am trying to authorize user only If user is admin he should be able to see all post and comments otherwise the normal user can see its own post only .I have read github page but was quite confusing
[post_controller.rb]
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def index
#posts = Post.all.order('created_at DESC')
end
def new
#post = Post.new
end
def show
#post = Post.find(params[:id])
end
def create
#post = Post.new(post_params)
#post.user = current_user
if #post.save
redirect_to #post
else
render 'new'
end
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
if #post.update(params[:post].permit(:title, :body))
redirect_to #post
else
render 'edit'
end
end
def destroy
#post = Post.find(params[:id])
#post.destroy
redirect_to posts_path
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
[comments_controller]
class CommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(params[:comment].permit(:name, :body))
#comment.user = current_user
redirect_to post_path(#post)
end
def destroy
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
#comment.destroy
redirect_to post_path(#post)
end
end
[ability.rb]
class Ability
include CanCan::Ability
def initialize(user)
unless user
else
case user.roles
when 'admin'
can :manage, Post
can :manage, Comment
when 'user' # or whatever role you assigned to a normal logged in user
can :manage, Post, user_id: user.id
can :manage, Comment, user_id: user.id
end
end
[comment.rb]
class Comment < ActiveRecord::Base
belongs_to :post
end
[post.rb]
class Post < ActiveRecord::Base
has_many :comments, dependent: :destroy
validates :title, presence: true, length: {minimum: 5}
validates :body, presence: true
end
[user.rb]
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
[migration]
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table(:users) do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
end
end
[migration]
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :name
t.text :body
t.references :post, index: true
t.timestamps
end
end
end
[migration]
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
It seems you do not yet have a user relationship to post and comment in which you need in order to identify if the user owns/created the comment/post
Run:
rails generate migration AddUserToPost user:belongs_to
rails generate migration AddUserToComment user:belongs_to
bundle exec rake db:migrate
Then add the association relationships:
post.rb
class Post < ActiveRecord::Base
belongs_to :user
# ..
end
comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
# ..
end
user.rb
class User < ActiveRecord::Base
has_many :posts
has_many :comments
# ..
end
Now you can identify who owns the post/comment, and what posts/comments a user owned/created with something like the following pseudo-code:
# rails console
post = Post.find(1)
post_owner = post.user
comment = Comment.find(1)
comment_owner = comment.user
user = User.find(1)
user_comments = user.comments
user_posts = user.posts
Now, the next step is to auto-associate the logged-in user to newly created posts/comments. This is done through the controllers:
posts_controller.rb
class PostsController < ApplicationController
authorize_resource
# ..
def create
#post = Post.new(post_params)
#post.user = current_user # I assume you have a variable current_user, or if you are using Devise current_user is already accessible
if #post.save
redirect_to #post
else
render :new
end
end
end
comments_controller.rb
class CommentsController < Application
authorize_resource
# ..
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(params[:comment].permit(:name, :body))
#puts "hhhhhhhhhh#{#comment}"
#comment.user = current_user # I assume you have a variable current_user, or if you are using Devise current_user is already accessible
#comment.save
redirect_to post_path(#post)
end
end
Now, at this point. Whenever a post/comment gets created, the logged-in user is automatically associated to it (as the owner).
Finally, we could just update the Ability class to only authorize users to :edit, :update, :show, and :destroy actions, if the user_id: current_user (logged-in user).
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
# if not logged in (Guest)
unless user
# cant do anything unless you add more `can` here
# else if logged in
else
case user.role
when 'admin'
can :manage, Post
can :manage, Comment
when 'normal' # or whatever role you assigned to a normal logged in user
can :manage, Post, user_id: user.id
can :manage, Comment, user_id: user.id
# If you don't have a role name for a normal user, then use the else condition like Rich Peck's answer. Uncomment the following instead, and then comment the `when 'normal' block of code just above
# else
# can :manage, Post, user_id: user.id
# can :manage, Comment, user_id: user.id
end
end
end
end
Just a final helpful information to the Ability above:
can :manage, Post, user_id: user.id
This is just a shorthand equal to:
can [:show, :edit, :update, :destroy], Post, user_id: user.id
can [:index, :new, :create], Post
You will notice that user_id: user.id is not taken into consideration for :index, :new, and :create because these are :collection methods, and not :member methods. More info here
If you want readability and customizability, you may opt to use the longer one above instead of the shorthand :manage.
#app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
case user.role
when "admin"
can :manage, :all
else
can :read, Post #-> cannot read comments
end
end
end
The above is how the ability class should look. You can replace the switch/case with if/else.
--
You're missing the evaluation of your objects, specifically with the can? & authorize methods:
#app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
#post = Post.find params[:post_id]
#comment = #post.comments.new comment_params
#comment.save if authorize! :create, #comment
redirect_to #post
end
def destroy
#post = Post.find params[:post_id]
#comment = #post.comments.find params[:id]
#comment.destroy if authorize! :destroy, #comment
redirect_to #post
end
private
def comment_params
params.require(:comment).permit(:name, :body)
end
end
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def show
#post = Post.find params[:id]
end
end
#app/views/posts/show.html.erb
<%= #post.title %>
<%= render #post.comments if can? :read, #post.comments %>
1) Change this line in PostsController, delete this condition: except [index, show]. Or user could see pages without authorization.
before_action :authenticate_user!
2) Change index action and other with this style. Use - current_user.
def index
if current_user.has_role? :admin
#posts = Post.all.order('created_at DESC')
else
#posts = current_user.posts.order('created_at DESC')
end
end
You can write you abilities in this way
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
case user.role
when "admin"
can :manage, :all
else
can :read, Post, :user_id => user.id
end
end
end
And just load resources of post using ability resource so that it only load post of current user if other than admin
class CommentsController < Application
load_and_authorize_resource
def index
#posts = #posts
end
end

How to access current_user variable in controller or model?

I have 1:N relationship between user and post model. I want to access user_id in post model. I tried it by accessing current_user but it's throwing cannot find current_user variable.
My userModel class:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :validatable
has_many :post
validates_format_of :email, with: /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
end
MyPostModel class:
class Post < ActiveRecord::Base
belongs_to :user
before_create :fill_data
validates_presence_of :name, :message => 'Name field cannot be empty..'
def fill_data
self.is_delete = false
self.user_id = current_user # here I am getting the error
end
end
MyPostController class
class PostController < ApplicationController
before_action :authenticate_user!
def index
#post = Post.all
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
if #post.save
redirect_to action: 'index'
else
render 'new'
end
end
.....
private
def post_params
params.require(:post).permit(:name,:user_id,:is_delete)
end
end
I can access the before_action :authenticate_user! in Post controller but not current_user in post model or controller. What I am doing wrong here in Post.fill_data. self.user_id?
Rest of the code is working fine and I can see the new entry of :name and :is_delete in sqlite3 database (when I am commenting self.user_id line in Post class).
Edit-1
I already have migration class for post
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.string :name
t.boolean :is_delete
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
In Rails your models should not be aware of the apps current user or any other state. They only need to know about themselves and the objects they are directly related to.
The controller on the other hand is aware of the current user.
So the proper way to do this would be to remove the fill_data callback from Post. And do it in the controller:
class PostController < ApplicationController
before_action :authenticate_user!
def index
#post = Post.all
end
def new
#post = current_user.posts.build
end
def create
#post = current_user.posts.build(post_params)
if #post.save
redirect_to action: 'index'
else
render 'new'
end
end
private
def post_params
params.require(:post).permit(:name,:user_id,:is_delete)
end
end
You should also set the default for your is_delete column in the database instead, but if you want to rock it like a pro use an enum instead.
Create a migration rails g migration AddStateToUsers and fill it with:
class AddStateToUsers < ActiveRecord::Migration
def change
add_column :users, :state, :integer, default: 0
remove_column :users, :is_delete
add_index :users, :state
end
end
We then use the rails enum macro to map state to a list of symbols:
class Post
enum state: [:draft, :published, :trashed]
# ...
end
That lets you do Post.trashed to get all posts in the trash or post.trashed? to check if a specific post is trashed.
notice that I use trashed instead of deleted because ActiveRecord has build in deleted? methods that we don't want to mess with.
You are trying to add current_user.id in post model using before_create call back. but better to do is use this
In posts_controller.rb
def new
#post = current_user.posts.new
end
def create
#post = current_user.posts.create(posts_params)
end
This will create a post for the current user.
Your fill_data method would be
def fill_data
self.is_delete = false
end

Resources