expiring action from a different controller - caching

I have two controllers like this
class New::StoresController < ApplicationController
caches_action :index, cache_path: Proc.new {|c| c.params}, expires_in: cache_time(:long)
caches_action :show, cache_path: Proc.new {|c| c.params}, expires_in: cache_time(:long)
def index
end
def show
# Here I use param[:store], params[:locale] and params[:ref]
end
end
class New::StoreReviewsController < ApplicationController
def create
expire_action controller: '/new/stores', action: :show, \
store:params[:store], locale:"en", ref:"stores"
end
end
I want to expire action New::StoresController#show on creation of a new StoreReview.
When the show action of Store is executed, the cache reads the following
Cache read: views/localhost:3000/en/stores/store-name
Read fragment views/localhost:3000/en/stores/store-name
While expire_action expires this fragment
Expire fragment views/localhost:3000/en/search?action=show&controller=new/stores&ref=stores
Cache delete: views/localhost:3000/en/search?action=show&controller=new/stores
The question is should I use expire_fragment instead of expire_action as mentioned here ? rails 3 caching: expire action for named route
Also if I want to cache the action but not with all params what is the best way to do it?
Currently that's what I am doing
caches_action :show, cache_path: Proc.new {|c| c.params.select{ |p| [:controller,:action ,"locale", "store"].include? p} }, expires_in: cache_time(:long)

I've got the answer for that.
By default caches_action ignores parameters.
And for getting the right fragment to be expired I did the following
expire_action store_url(host: my_host, store: store_name, locale: locale])

Related

Rails/Devise/Pundit : Redirect after login if next action not authorized

I am using Rails 7, Devise and Pundit.
I've got Users and Projects.
Only Users classified as "admin" or "moderator" can perform actions (New,
Edit, Update, Destroy, ...).
Unlogged Users and Users classified as "user" can see Index and Show pages.
When I'm on a show page ('http://localhost:3000/projects/[id]') as an unlogged User and try to edit it (via 'http://localhost:3000/projects/[id]/edit') it sends me to a Devise login page which is normal. Once logged in correctly with an unauthorized profile (User classified as "user") Pundit authorization kicks in and rescues the request.
=> The problem is here :
First Firefox tells me that the page isn't redirected properly ... Probably because I'm sent back to 'http://localhost:3000/users/sign_in' while being signed in.
When I reload my page it tells me via an alert "You are already signed in." on my root_path page.
Application_controller :
class ApplicationController < ActionController::Base
before_action :store_user_location!, if: :storable_location?
before_action :authenticate_user!, except: [:index, :show]
before_action :configure_permitted_parameters, if: :devise_controller?
include Pundit
protect_from_forgery with: :exception
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
after_action :verify_authorized, except: :index, unless: :skip_pundit?
after_action :verify_policy_scoped, only: :index, unless: :skip_pundit?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
devise_parameter_sanitizer.permit(:sign_in, keys: [:username])
devise_parameter_sanitizer.permit(:account_update, keys: [:username])
end
private
def skip_pundit?
devise_controller? || params[:controller] =~ /(^(rails_)?admin)|(^pages$)/
end
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back(fallback_location: root_path)
end
def storable_location?
request.get? && is_navigational_format? && !devise_controller? && !request.xhr?
end
def store_user_location!
# :user is the scope we are authenticating
store_location_for(:user, request.fullpath)
end
def after_sign_in_path_for(resource_or_scope)
stored_location_for(resource_or_scope) || super
end
end
Project_policy :
class ProjectPolicy < ApplicationPolicy
class Scope < Scope
# NOTE: Be explicit about which records you allow access to!
# def resolve
# scope.all
# end
def resolve
scope.all
end
private
attr_reader :user, :scope
end
def index?
true
end
def show?
true
end
def create?
user.admin? || user.moderator?
end
def edit?
user.admin? || user.moderator?
end
def update?
user.admin? || user.moderator?
end
def destroy?
user.admin? || user.moderator?
end
end
I don't think more is needed but if some code samples are missing don't hesitate to tell me ! I'd like to find a way to handle this properly. Thanks !
I know it is in the Pundit documentation but have you tried without the protect_from_forgery line? I can tell you from first hand experience Pundit works without it...
EDIT: Try to move the protect_from_forgery before the before_action :authenticate_user!
I found a solution but it's probably not a clean one and I don't know if it is safe or if it's durable.
I removed from my application_controller the following :
the method : storable_location?
the method : store_user_location!
before_action :store_user_location!, if: :storable_location?
This is what I added/modified under "private".
# Redirect after login via Devise
def after_sign_in_path_for(resource)
session["user_return_to"] || root_path
end
# Redirect if unauthorized by Pundit
def user_not_authorized
session["user_return_to"] = redirection_reroll
flash[:alert] = "You are not authorized to perform this action."
redirect_to(session["user_return_to"] || root_path)
end
# Reroll redirection path when unauthorized
def redirection_reroll
checker = ["new", "edit"]
path = session["user_return_to"].split("/")
if checker.include? path[-1]
path.pop()
end
session["user_return_to"] = path.join("/")
end

Do I have to specify actions in routes.rb?

While I'm reading Rails 4 in Action, I'm trying to implement my own application, so it doesn't look same as in the book.
The book's corresponding commit is Section 7.2.3: Only admins can create or delete projects
In my case, admins can only delete the item (item corresponds to the project in the book.).
My repo https://github.com/tenzan/shop and deployed http://ichiba-demo.herokuapp.com/
The rule I want to apply is:
A regular user (you can login with staff#example.com/password) can do everything except destroy action.
Admin (admin#example.com/password) only can destroy.
To realise that I have:
In admin/items_controller.rb:
class Admin::ItemsController < Admin::ApplicationController
def destroy
#item = Item.find(params[:id])
#item.destroy
flash[:notice] = 'Item has been deleted.'
redirect_to items_path
end
private
def item_params
params.require(:item).permit(:name, :quantity)
end
end
In controllers/items_controller.rb:
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update]
def index
#items = Item.all
end
def new
#item = Item.new
end
def create
#item = Item.new(item_params)
if #item.save
flash[:notice] = 'Item has been created.'
redirect_to #item
else
flash.now[:alert] = 'Item has not been created.'
render 'new'
end
end
def show
end
def edit
end
def update
if #item.update(item_params)
flash[:notice] = 'Item has been updated.'
redirect_to #item
else
flash.now[:alert] = 'Item has not been updated.'
render 'edit'
end
end
private
def set_item
#item = Item.find(params[:id])
rescue ActiveRecord::RecordNotFound
flash[:alert] = 'The item could not be found.'
redirect_to items_path
end
def item_params
params.require(:item).permit(:name, :quantity)
end
end
In routes.rb:
Rails.application.routes.draw do
namespace :admin do
root 'application#index'
resources :items, only: :destroy
end
devise_for :users
root 'items#index'
resources :items, only: [:index, :show, :edit, :update, :new, :create] do
resources :comments
end
end
Questions:
Do I have to specify actions in the routes.rb, as I already have specified who can use what actions in their corresponding controllers? I didn't notice any change when I remove them from the routes.rb...
Am I violating DRY concept, when I specify actions in 2 places, i.e. in the routes.rb and controllers/items_controllers.rb ?
I'll be happy if you point out other places to improve to meet best practice.
PS: The subject maybe vague, please feel free to edit it.
Do I have to specify actions in the routes.rb, as I already have
specified who can use what actions in their corresponding controllers?
Yes. For instance, if you'd have only one action in items_controller.rb controller (let say show), and left
resources :items do # no specified actions
#...
end
in routes.rb it would generate all routes for items controller (for new, create, edit, destroy, update etc). But specifying actions in routes.rb you limit generated routes to only needed.
Am I violating DRY concept, when I specify actions in 2 places, i.e.
in the routes.rb and controllers/items_controllers.rb ?
No. Because you actually specify actions in controller, in routes.rb you only specify routes.
I'll be happy if you point out other places to improve to meet best
practice.
This line:
resources :items, only: [:index, :show, :edit, :update, :new, :create] # better to use %i() notation, eg only: %i(index show edit update new create)
could be written as:
resources :items, except: :destroy
Regarding your admin user - to allow only him to destroy, just check if current_user is admin. If you'll have more, than one action which is allowed to be performed by only admin, you can create before_action in controller:
before_action :check_admin?, only: %i(destroy another_action)
private
def check_admin?
# your logic to check, if user is admin
end
You can also be interested in going through Ruby style guide.
Even though you're not violating DRY directly, you're muddying up the REST architecture by moving a single entity's actions to different controllers. You don't need a specific controller or namespace for admins - you just need to assert that the user is an administrator before proceeding with the delete action.
Since you have already added the admin column to your devise model, you can move the delete action to ItemsController
def destroy
if current_user.try(:admin?)
#item = Item.find(params[:id])
#item.destroy
flash[:notice] = 'Item has been deleted.'
else
flash[:alert] = 'Only admins can delete items.'
end
redirect_to items_path
end
Your routes would be cleaner since your admin namespace would be used only for user moderation. The only route for items would be:
resources :items do
resources :comments
end

Devise not storing sessions and losing credentials after redirect

It is a VERY strange bug and I am leading with it for 24 hours. It was working well and suddenly it started to fail.
The problem:
When I want to login with Facebook, the app redirec to Facebook permissions request, go back, save the update in the account model (access_token, and updated_at), but I am redirected to the home without permissions to access to signed_in sections.
My stack is:
Rails4, Devise 3.0.0.rc, Omniauth, Omniauth-facebook 1.4.0.
The app only accept login with Facebook.
Take a look:
Omniauth controller: account_signed_in? = true
class Accounts::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
# You need to implement the method below in your model (e.g. app/models/user.rb)
#account = Account.find_for_facebook_oauth(request.env["omniauth.auth"], current_account)
if #account.persisted?
sign_in_and_redirect #account, :event => :authentication #this will throw if #user is not activated
puts account_signed_in? # <-- true
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_account_registration_url
end
end
ApplicationController: account_signed_in? = true
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
private
def stored_location_for(resource_or_scope)
nil
end
def after_sign_in_path_for(resource_or_scope)
puts account_signed_in? # <-- true
current_account.pages.empty? ? new_page_path : pages_path
end
StaticController (home) account_signed_in? = false
class StaticController < ApplicationController
def home
puts account_signed_in? # <- false
render layout: 'home'
end
I don't know if can there be something that disturb the normal flow of sessions between Devise and Rails.
Found that!
The sessions weren't saved because of the domain parameter in session_store.rb:
BrainedPage::Application.config.session_store :cookie_store,
key: '_my_session', :domain => Rails.configuration.domain
Seems I had changed the domain configuration in development environment (added port, because I was using this var for other propose too), and I didn't realize the impact it could make.

rspec controller post action test fails with ActiveModel::ForbiddenAttributesError

I want to test the post action of my controller but it fails with an ActiveModel::ForbiddenAttributesError.
require 'spec_helper'
describe HomeController do
describe "POST #create" do
context "with valid attributes" do
it "saves the new feed in the database" do
expect{
post :create, feed: FactoryGirl.attributes_for(:valid_feed)
}.to change(Feed,:count).by(1)
end
end
end
Factory:
FactoryGirl.define do
factory :valid_feed, class: Feed do
title "CRE"
address "http://cre.fm/feed/m4a/"
end
factory :invalid_feed, parent: :valid_feed do |f|
f.address nil
end
end
Controller:
class HomeController < ApplicationController
def index
end
def create
#feed = Feed.new(params[:feed])
if #feed.save
redirect "/"
else
render nothing: true
end
end
end
I checked the params in controller to see if there is something wrong but params[:feed] contains a valid hash.
Did I miss something here?
Yup, you missed something. Rails 4 has enabled strong parameters by default. You'll need to change it to something like this:
class HomeController < ApplicationController
def index
end
def create
#feed = Feed.new(feed_params)
if #feed.save
redirect "/"
else
render nothing: true
end
end
private
def feed_params
params.require(:feed).permit(:allowed, :elements, :of, :feed)
end
end

in UsersController#create, User.new(params[:user]) return an empty User (params looks good)

I'm kind of new to Rails 3.1. and I'm facing an issue only in my production env with my Signup form (actually, it's more about the controller).
Here is the code in User
class UsersController < ApplicationController
[...]
def create
#user = User.new(params[:user])
logger.info "value of login in param : #{params[:user][:login]}" #-> log the actual login
logger.info "value of login : #{#user.login}" #-> log empty
#user.admin = false
if #user.save
flash[:notice] = t('flash.notice.user.create.valid')
redirect_back_or_default root_path
else
flash[:notice] = t('flash.notice.user.create.invalid')
render :action => :new
end
end
end
Also, the controller logs show that the params hash is good
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"QwOqmp0CT/d4mmC1yiLT4uZjP9bNDhbUXHanCQy5ZrA=",
"user"=>{"login"=>"myLogin",
"email"=>"t.r#gmail.com",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]"}}
My login form works as expected (already created users are able to sign in)
Again, this only happens in production.
EDIT: Here is my User Model
class User < ActiveRecord::Base
acts_as_authentic
#== Callbacks
before_create :set_defaults
attr_accessible :avatar ##### EDIT: TO FIX THE ISSUE, ADD THE OTHER FIELDS AS WELL
protected
def set_defaults
self.total_1 = self.total_2 = self.total_3 = 0
end
end
Just to memorialize the answer from the comments above:
Normally you can use mass assignment to set fields on a model, but when you use attr_accessible, you are then limited to only mass assigning those fields. So stuff like User.new(params[:user]) won't work; instead, you'd have to do:
#user = User.new
#user.login = params[:user][:login]
# ...etc.
#user.save
Simple add your fields to the attr_accessible list and you can go back to mass assignment.

Resources