We are building a Rails4 app using Trailblazer. I have never worked with Trailblazer before and I am confused about how to do things.
We are building an auction site. I was previously using a traditional controller, and this route endpoint was working fine:
def bill
#profile = Profile.find_by user_id: current_user_id
#current_order = Order.order(created_at: :desc).find_by(user_id: current_user_id)
#batch = #current_order.batch
if #batch.nil?
puts "There was no batch linked to the current order of #{#current_order.id}"
flash[:error] = "We are sorry, but we could not determine which batch your order belongs to."
else
#price_shown_to_customer = #batch.price + ENV["FUELBID_FEE_PER_GALLON"].to_f
#amount = #current_order.quantity * #price_shown_to_customer
end
But now I'm suppose to create this as a Trailblazer api, using a Representer class.
So in routes.rb I added something for "charges":
namespace :api do
get '/price' => 'info#info'
post '/order' => 'orders#create'
get '/charges' => 'charges#bill'
end
I created this Api but copying-and-pasting another:
module Api
class ChargesController < ApiApplicationController
respond_to :json
def bill
respond_with OpenStruct.new.extend(ChargesRepresenter)
end
end
end
I tested the above with a simple Representer and it all worked fine, so everything is good up to this point. If I return simple data from the Representer, then I can see it fine here:
http://localhost:3000/api/charges.json
But I need to get the current_user. How is this done? Right now, this does not work:
module ChargesRepresenter
include Roar::JSON
collection :price_shown_to_customer
def price_shown_to_customer
current_order = Order.order(created_at: :desc).find_by(user_id: current_user_id)
puts "current_order"
puts current_order.id
batch = current_order.batch
batch.price + ENV["FUELBID_FEE_PER_GALLON"].to_f
end
end
current_user_id exists in my traditional controllers because we set up Devise and so my traditional controllers inherit it:
class ChargesController < SecuredController
But is there any way to get it in a Trailblazer Representer?
Hope this answer is not too late.
If you can switch to Decorator pattern instead of a Module.
Representer really doesn't need to know and doesn't care if it is called from controller or console or test. All it needs is a hash to build your json object from. So you can just pass another attribute called current_user_id to your Representer and then use it inside r presenter like you do.
FYI:
If you need a more immediate response you can also copy your question to https://gitter.im/trailblazer/chat . There are usually several people hanging out there. But it's also good to post a question here for posterity.
Related
I'm new to Rails and I'm working through Michael Hartl's excellent Rails Tutorial for a second time, this time I'm trying to adapt the chapter 11 and chapter 12 microposts to a simple Devise/Pundit application I'm working on. I am able to create microposts through the seed file and display them, but I'm getting an authorization error with Pundit when I actually try to create a new post through the site. The error I'm getting is:
Pundit::AuthorizationNotPerformedError in MicropostsController#create
My Microposts Controller looks like this:
class MicropostsController < ApplicationController
before_action :authenticate_user!
after_action :verify_authorized
def create
#micropost = current_user.microposts.build(micropost_params)
if #micropost.save
flash[:success] = "Micropost created!"
redirect_to current_user
else
#feed_items = []
flash[:danger] = "Unable to create micropost!"
end
end
def destroy
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
end
I'm thinking that do not have the authorization set up properly for the 'create' action, but I'm not sure exactly how it should be set. I do not have a policy for Pundit for Microposts. I tried adding a simple one but it didn't change anything. I'm learning to put all these pieces together, would someone point me in the right direction?
There is one after action filter verify_authorized because of which you are getting this error. If you have created a policy for the create action then use that to get rid of the error.
So I am trying to override Devise::RegistrationsController which they do have wiki for and tons of tutorial out there. The one thing that I can not find is the best implementation of how to override the controller whilst implementing the require admin approval feature as well.
I think I got the hang of it but before I go any further (from all the reading on the Devise's source code) I want to know, on the registrations controller there's a line that does:
resource.active_for_authentication?
However, on the Sessions controller it's just this:
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_flashing_format?
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
What I want to know is, if it's not confirmed or the active_for_authentication returns false, where or how does the session controller check this? I tried tracing back the source code but no luck.
So anyone who's very familiar with Devise perhaps you could answer my question? Thank you.
After authenticating a user and in each request, Devise checks if your model is active by calling model.active_for_authentication?. This method is overwritten by other devise modules. For instance, :confirmable overwrites .active_for_authentication? to only return true if your model was confirmed.
You can overwrite this method yourself, but if you do, don't forget to call super:
def active_for_authentication?
super && special_condition_is_valid?
end
Whenever active_for_authentication? returns false, Devise asks the reason why your model is inactive using the inactive_message method. You can overwrite it as well:
def inactive_message
special_condition_is_valid? ? super : :special_condition_is_not_valid
end
I have just made the switch to Pundit from CanCan. I am unsure about a couple of things, and how Pundit is best used.
For example:
If you have a resource that can have multiple parent objects, for instance lets say a Goal belongs to a student and instructor. Therefor, a student can have many goals and an instructor can have many goals. In a controller index action you might do:
if params[:student_id].present?
#account = Student.find(params[:student_id])
#goals = #account.goals
elsif params[:instructor_id].present?
#account Instructor.find(params[:instructor_id])
#goals = #account.goals
end
params are not usable inside policies, so the logic needs to be done here. I think. For what I can tell, if you skip the policy_scope you will get an unauthorized error when viewing the index page for goals.
Would you:
#goals = policy_scope(#account.goals)
OR
#goals = policy_scope(Goal.scoped).where( account_id: #account.id)
What happens when you throw a bunch of includes in the mix?
#example = policy_scoped(#school.courses.includes(:account => :user, :teacher ))
Or when needed to order...is this correct?
policy_scope(Issue.scoped).order("created_at desc")
When using scopes: What is :scope here? Is :scope an instance of the model being evaluated? I've tried accessing its attributes via :scope, but didn't work.
class Scope < Struct.new(:user, :scope)
Reading through this from a security perspective I can see a couple things that bear mentioning. For example, if you are allowing users to specify the student_id and instructor_id param fields, what's to stop them from passing in an ID for someone other than themselves? You don't ever want to let a user specify who they are, especially when you are basing policies on the users type.
For starters, I would implement Devise and add an additional boolean field called instructor that would be true when the user was an instructor but default to false for students.
Then your Users would automatically have an instructor? method defined, which will return true if the value in the instructor column is true.
You could then add a helper for students:
def student?
!instructor?
end
Now using Devise (which gives us access to a current_user variable) we can do things like current_user.instructor? which will return true if they are an instructor.
Now on to the policy itself. I just started using Pundit a few weeks ago, but this is what I'd do in your situation:
class GoalPolicy < ApplicationPolicy
class Scope < GoalPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
#scope.where(user: #user)
end
end
end
Then your (I'm assuming GoalsController class and index method) method can look like:
def index
policy_scope(Goal) # To answer your question, Goal is the scope
end
If you wanted to order you could also do
def index
policy_scope(Goal).order(:created_at)
end
I just realized that you asked this question half a year ago, but hey! Maybe it'll answer some questions other people have and maybe I'll get some feedback on my own budding Pundit skills.
Follow #Saul's recommendation on adding devise or other means of authentication.
Then you'll want to do this (Entity is Goal in your case):
#entities = policy_scope(Entity).where(...)
In entity_policy.rb:
class EntityPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
# Here you have access to `scope == Entity` and `user == current_user`
scope.where(entity: user.entity)
end
end
end
You might wonder why is where duplicated. The answer is (and here is the answer to your question): they serve different purposes. Although currently they are identical, but consider this:
You now have an admin user who has access to everything. Your policy changes:
class EntityPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
if user.admin?
scope.all
else
scope.where(entity: user.entity)
end
end
end
end
If you have organizations with goals and the following restful endpoint:
/organizations/:organization_id/goals
When a user visits /organizations/1/goals you want to make sure the user is only allowed access to goals when the user is part of the organization:
scope.where(organization: user.organization) in the policy
And you also want to make sure that when an admin visits they can only see the goals related to that organization:
policy_scope(Goal).where(organization_id: params[:organization_id]) in the controller.
As preface, I've followed through some tutorials (i.e. Michael Hartl's) though I'm still fairly novice. Forgive any cloudy terminology.
I am trying to build a simple application in Rails 4 that does the following:
User logs into application (currently working with sign-in-with-twitter link and routing)
get "/auth/:provider/callback" => "sessions#create"
get "/signout" => "sessions#destroy", :as => :signout
Once <% if current_user %> is true, I have the view rendering a partial where there will be a list of simple buttons. When the user clicks a button I want the application to tweet on behalf of the current_user a preset string. Ideally, I'd do this all in ruby/rails.
These button functions are where I'm getting hung up. I've read a fistful of documents but there seem to be a lot of conflicting and old answers. Here's a quick list of the ones I think are closest, though not explicit about sending a tweet from a simple button/link in a view:
http://www.sitepoint.com/ruby-social-gems-twitter/
http://richonrails.com/articles/sending-a-tweet-to-twitter
Some call for controllers, a more robust oauth setup (which I have bundle installed and connected to the dev.twitter application, though not fleshed out beyond keys), and whatever else. It's got me turned around and I'm not yet good enough to synthesize all the information. Any help and direction would be great. Below are some other files in the app that might be helpful.
class SessionsController < ApplicationController
def create
auth = request.env["omniauth.auth"]
user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) || User.create_with_omniauth(auth)
session[:user_id] = user.id
redirect_to root_url, :notice => "Hi!"
end
def destroy
session[:user_id] = nil
redirect_to root_url, :notice => "Bye!"
end
end
And omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter, '_priv', '_priv'
end
Eep! I'm the author of the second link (RichOnRails). Did you take a look at the example app included with the tutorial? It does almost exactly what you want. If the tweets are hard coded you could approach it in a couple of different ways. If you take a look at the tweets controller, you'll see it takes a parameter called 'message'. Any message passed to that create method will tweet as the current user.
def create
current_user.tweet(twitter_params[:message])
end
The easiest (but not necessarily best) way to adapt this to fit your needs is to have a form for each tweet, and do a hidden field with the message you wish to tweet. The button becomes a submit for that particular form (you can add remote: true if you want to keep the page from refreshing, then use a bit of javascript to update the UI elements). Hope this helps.
Yea, I know that this question is silly, newbee and simple, but I still can't figure it out.
I've created a class (in app/minions/ directory) to parse auth hashes from 3rd-party services (like google, twitter, etc.). It looks like this.
class AuthHash
def initialize(hash)
#hash = hash
#provider = hash[:provider]
#uid = hash[:uid]
create_user_hash
end
def create_user_hash
#user_hash = send("parse_hash_from_" << #hash[:provider], #hash)
end
def credentials
{provider: #provider, uid: #uid}
end
def user_hash
#user_hash
end
private
# parse_hash_from_* methods here
end
I've added that directory to the autoload path, so I can use it in my controllers. Now I want to write some tests for it.
I'm using RSpec with FactoryGirl for testing. So I started by adding a factory to spec/factories/ called auth_hashes.rb but I can't define a hash as a field in a factory.
So I moved the declaration to the spec/minions/auth_hash_spec.rb.
require 'spec_helper'
describe AuthHash do
before_each do
auth_hash = AuthHash.new({:provider=>"google_oauth2",:uid=>"123456789",:info=>{:name=>"JohnDoe",:email=>"john#company_name.com",:first_name=>"John",:last_name=>"Doe",:image=>"https://lh3.googleusercontent.com/url/photo.jpg"},:credentials=>{:token=>"token",:refresh_token=>"another_token",:expires_at=>1354920555,:expires=>true},:extra=>{:raw_info=>{:id=>"123456789",:email=>"user#domain.example.com",:verified_email=>true,:name=>"JohnDoe",:given_name=>"John",:family_name=>"Doe",:link=>"https://plus.google.com/123456789",:picture=>"https://lh3.googleusercontent.com/url/photo.jpg",:gender=>"male",:birthday=>"0000-06-25",:locale=>"en",:hd=>"company_name.com"}}})
end
end
But still it does not seem to work.
I know this should be alot simpler then I'm trying to do, but I can't figure it out.
Add something like this to that new spec (spec/minions/auth_hash_spec.rb) file at the top:
require Rails.root.to_s + '/app/minions/myhash.rb'
And then write your tests.