I'm following a tutorial on how to create a REST Api using the rails-api gem.
In the tutorial, most of the API logic is encapsulated in the base class file app/controllers/api/v1/api_base_controller.rb
module Api
module V1
class ApiBaseController < ApplicationController
protect_from_forgery with: :null_session
before_action :set_resource, only: [:destroy, :show, :update]
respond_to :json
private
#Returns the resource from the created instance variable
##return [Object]
def get_resource
instance_variable_get("##{resource_name}")
end
#Returns the allowed parameters for searching
# Override this method in each API controller
# to permit additional parameters to search on
# #return [Hash]
def query_params
{}
end
#Returns the allowed parameters for pagination
# #return [Hash]
def page_params
params.permit(:page, :page_size)
end
# The resource class based on the controller
# #return [Class]
def resource_class
#resource_class ||= resource_name.classify.constantize
end
# The singular name for the resource class based on the controller
# #return [String]
def resource_name
#resource_name ||= self.controller_name.singularize
end
#Only allow a trusted parameter "white list" through.
# If a single resource is loaded for #create or #update,
# then the controller for the resource must implement
# the method "#{resource_name}_params" to limit permitted
# parameters for the individual model.
def resource_params
#resource_params ||= self.send("#{resource_name}_params")
end
#Use callbacks to share common setup or constraints between actions.
def set_resource(resource = nil)
resource ||= resource_class.find(params[:id])
instance_variable_set("##{resource_name}", resource)
end
#POST /api/v1/{plural_resource_name}
def create
set_resource(resource_class.new(resource_params))
if get_resource.save
render :show, status: :created
else
render json: get_resource.errors, status: :unprocessable_entity
end
end
#DELETE /api/v1/{plural_resource_name}/:id
def destroy
get_resource.destroy
head :no_content
end
#GET /api/v1/{plural_resource_name}
def index
plural_resource_name = "##{resource_name.pluralize}"
resources = resource_class.where(query_params).page(page_params[:page]).per(page_params[:page_size])
instance_variable_set(plural_resource_name, resources)
respond_with instance_variable_get(plural_resource_name)
end
#GET /api/v1/{plural_resource_name}/1
def show
respond_with get_resource
end
#PATCH/PUT /api/{plural_resource_name}/1
def update
if get_resource.update(resource_params)
render :show
else
render json: get_resource.errors, status: :unprocessable_entity
end
end
end
end
end
The model controllers inherit from this ApiBaseController
one of the model controllers (albums_controllers.rb) looks like this:
module Api
module V1
class AlbumsController < Api::V1::ApiBaseController
private
def album_params
params.require(:album).permit(:title)
end
def query_params
params.permit(:title, :artist_id)
end
end
end
end
I also have this in routes.rb
Rails.application.routes.draw do
namespace :api, defaults: {format: 'json'} do
namespace :v1 do
resources :albums, :artists
end
end
end
when I do http://localhost:3000/api/v1/artists on the browser, i get this error:
`The action 'index' could not be found for Api::V1::ArtistsController`
I have also noted that the ApplicationController generated by the rails-api gem inherits from ActionController::API and not ActionController::Base but i'm not sure whether this is the problem.
private methods are not available to subclasses. If you REALLY want to hide all of those methods but make it available to subclasses, use protected instead.
Related
Doing API for my first Rails project.
I have base class ApiController for all the APIs:
module Api
class ApiController < ::ApplicationController
respond_to :json
skip_before_action :verify_authenticity_token
def index
#collection = resource_class.all
render json: #collection.as_json(as_json_collection)
end
private
def resource_class
raise NotImplementedError
end
def as_json_collection
{}
end
end
end
And I have child class UsersController:
module Api
class UsersController < ApiController
private
def resource_class
User
end
def resource_params
params.require(:user).permit(:name, :email)
end
end
end
My routes:
namespace :api do
resources :users
end
Then I'm going to my_app/api/users I have error:
The action 'index' could not be found for Api::UsersController
But then I changing UsersController writing it's own index class, everything works fine and I'm having all my Users in JSON format.
I've alrady tried to comment all private marks in both classes, but that doesn't help.
I don't want to write an API for every entity in my project and I'd like to avoid this problem in future.
I got it to work with this:
module Api
class ApiController < ::ApplicationController
def index
respond_to do |format|
format.json { render json: '"abc"' }
end
end
end
end
module Api
class UsersController < ApiController
end
end
The URL was http://localhost:3000/api/users.json
So for you I suggest:
module Api
class ApiController < ::ApplicationController
def index
respond_to do |format|
format.json do
#collection = resource_class.all
render json: #collection.as_json(as_json_collection)
end
end
end
end
end
module Api
class UsersController < ApiController
def resource_class
User
end
def resource_params
params.require(:user).permit(:name, :email)
end
end
end
Its supposed to be like this:
class Api::ApiController < ApplicationController
and do not forget to remove extra end, end of the file!
#sample
- api(folder)
-- api_controller.rb (Api::ApiController < ApplicationController)
-- users_controller.rb (Api::UsersController < Api::ApiController)
application_controller.rb
You need to read this my friend:
rails routes
When you do this:
namespace :api do
resources :users
end
rails creates CRUD routes automatically which means that my_app/api/users will translate to: .../users#index.
Do this to see your routes created by rails:
rails routes and for specific word (e.g. user): rails routes | grep user
Seeing is believing ;)
I have an application that is using both Devise and Knock. It is using Devise to power the authentication for Active Admin and Knock gem is providing the authentication for my API's
The issue I have is that Knock can't seem to find current_user and I believe this is likely because I am using Devise in the same project.
I have the following setup:
Api Controller
class ApiController < ActionController::API
include Knock::Authenticable
end
User Controller (for API not ActiveAdmin)
module Api
module V1
class UsersController < ApiController
before_action :set_user, only: [:show, :update, :destroy]
# GET /users/1
def show
render json: #user
end
# POST /users
def create
#user = User.new(user_params)
if #user.save
render json: #user.id, status: :created
else
render json: #user.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /users/1
def update
if #user.update(user_params)
render json: #user
else
render json: #user.errors, status: :unprocessable_entity
end
end
# DELETE /users/1
def destroy
#user.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#user = User.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
end
end
Auth Controller
module Api
module V1
class AuthController < ApiController
def auth
render json: { status: 200, user: current_user }
end
end
end
end
Current User in this Auth controller returns nothing however in another project I have, without devise, this will correctly return the user.
Is there a way to redefine what current_user is or assign it to something different for the purposes of using Knock?
Try this in your ApplicationController
# JWT: Knock defines it's own current_user method unless one is already
# defined. As controller class is cached between requests, this method
# stays and interferes with a browser-originated requests which rely on
# Devise's implementation of current_user. As we define the method here,
# Knock does not reimplement it anymore but we have to do its thing
# manually.
def current_user
if token
#_current_user ||= begin
Knock::AuthToken.new(token: token).entity_for(User)
rescue
nil
end
else
super
end
end
private
# JWT: No need to try and load session as there is none in an API request
def skip_session
request.session_options[:skip] = true if token
end
# JWT: overriding Knock's method to manually trigger Devise's auth.
# When there is no token we assume the request comes from the browser so
# has a session (potentially with warden key) attached.
def authenticate_entity(entity_name)
if token
super(entity_name)
else
current_user
end
end
To have a cleaner code I want to split my controller in some concerns.
In my routes.rb how to redirect to concern without redefine the methods of concern index show destroy create ...
class SomeController
include SomeConcern
def index
end
end
module SomeConcern
def index
end
end
Sorry for my bad english.
Lets say we have a CarsController and AirplanesController that have the typical create and new actions.
class AirplanesController < ApplicationController
def new
#airplane = Airplane.new
end
def create
#airplane = Airplane.new(create_params)
if #airplane.save
redirect_to #airplane
else
render :new
end
end
# ...
end
class CarsController < ApplicationController
def new
#car = Car.new
end
def create
#car = Car.new(create_params)
if #car.save
redirect_to #car
else
render :new
end
end
# ...
end
To dry this up we can extract the shared code to a module:
module Createable
extend ActiveSupport::Concern
included do
attr_accessor :resource
alias_attribute :self.controller_name.to_sym, :resource
end
def new
#resource = resource_class.new
yield #resource if block_given?
end
def create
#resource = resource_class.new(create_params)
if #resource.save
yield #resource if block_given?
redirect_to #resource
else
render :new
end
end
private
def create_params
raise "not implemented controller!"
end
def resource_class
#resource_class ||= self.controller_name.classify.constantize
end
end
We can then apply it to the controller classes by:
class CarsController < ApplicationController
include Createable
def create_params
params.require(:car)
.permit(:model) # ...
end
end
class AirplanesController < ApplicationController
include Createable
def create_params
params.require(:airplane)
.permit(:model) # ...
end
end
But a very important point here is that you are not routing to the module. The module is providing methods to the controller class.
You have to always map to your controller. Concerns are modules where you can put shared logic (it makes sense only in case you need 2 absolutely similar methods in 2 different controllers).
I think, that such code should work:
class SomeController
include SomeConcern
end
module SomeConcern
def index
end
end
Isn't it?
But concerns mostly used to move out some private helper methods from controller, rather actions as we do in this code piece
I have used the Pundit Gem before, but I've never tried doing what I'm trying to do now, and for some reason Pundit is not happy.
What I'm aiming to do, is to have a modal with the 'create' (Foo) form on my 'index'(Foos) page. Thus I need to instantiate an empty Foo object for the modal form to work.
The issue that I'm experiencing, is that Pundit throws an error when I submit the form remotely. The error is:
Pundit::NotDefinedError - unable to find policy of nil
I have tried to understand why this is happening but I've not been able to solve it yet.
Here is my foos_controller.rb#index:
...
def index
#foo = Foo.new
authorize #foo, :new?
#foos = policy_scope(Foo)
end
...
I then have the following 'before_action' filter that runs for my other actions i.e. 'create'
...
before_action :run_authorisation_check, except: [:index]
def run_authorisation_check
authorize #foo
end
...
The policies that I'm using in foo_policy.rb:
....
def index?
user.has_any_role? :super_admin
end
def create?
user.has_any_role? :super_admin
end
def new?
create?
end
def scope
Pundit.policy_scope!(user, record.class)
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.has_any_role? :super_admin
scope.all
end
end
end
....
The error does not present itself until I submit the form. Could anybody familiar with Pundit please help guide me to understand what I'm doing incorrectly?
UPDATE
Full foos_controller.rb
class FoosController < ApplicationController
def index
#foo = Foo.new
authorize #foo, :create?
#foos = policy_scope(Foo)
end
def new
#foo = Foo.new
end
def create
#foo = Foo.new(foo_params)
respond_to do |format|
if #foo.save
flash[:notice] = I18n.t("foo.flash.created")
format.json { render json: #foo, status: :ok }
else
format.json { render json: #foo.errors, status: :unprocessable_entity }
end
end
end
private
before_action :run_authorisation_check, except: [:index]
def foo_params
params.fetch(:foo, {}).permit(:bar)
end
def run_authorisation_check
authorize #foo
end
end
Yeah, you're not setting the value of #foo, that's why you're getting the error unable to find policy of nil.
Most times, you would have something like this in your foos_controller.rb:
before_action :set_foo, only: [:show, :edit, :update, :destroy]
before_action :run_authorisation_check, except: [:index]
...
private
def set_foo
#foo = Foo.find(params[:id])
end
Let me know if that works
I had this issue when working on a Rails 6 API only application with the Pundit gem.
I was running into the error below when I test my Pundit authorization for my controller actions:
Pundit::NotDefinedError - unable to find policy of nil
Here's how I solved:
Say I have a policy called SchoolPolicy:
class SchoolPolicy < ApplicationPolicy
attr_reader :user, :school
def initialize(user, school)
#user = user
#school = school
end
def index?
user.admin?
end
def show?
user.admin?
end
def new
create?
end
def edit
update?
end
def create
user.admin?
end
def update?
user.admin?
end
def destroy?
user.admin?
end
end
Then in my SchoolsController, I will have the following:
class Api::V1::SchoolsController < ApplicationController
before_action :set_school, only: [:show, :update, :destroy]
after_action :verify_authorized, except: :show
# GET /schools
def index
#schools = School.all
authorize #schools
render json: SchoolSerializer.new(#schools).serializable_hash.to_json
end
# GET /schools/1
def show
render json: SchoolSerializer.new(#school).serializable_hash.to_json
end
# POST /schools
def create
#school = School.new(school_params)
authorize #school
if #school.save
render json: SchoolSerializer.new(#school).serializable_hash.to_json, status: :created, location: api_v1_school_url(#school)
else
render json: #school.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /schools/1
def update
authorize #school
if #school.update(school_params)
render json: SchoolSerializer.new(#school).serializable_hash.to_json
else
render json: #school.errors, status: :unprocessable_entity
end
end
# DELETE /schools/1
def destroy
authorize #school
#school.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_school
#school = School.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def school_params
params.require(:school).permit(:name, :alias, :code)
end
end
Note:
I used an after_action callback to call the verify_authorized method to enforce authorization for the controller actions
I did not call the authorize method on the show action because it was skipped for authorization by me out of choice based on my design.
The instance variables called by the authorize method corresponds to the instance variable of the controller actions being called. So for the index action it is #schools and for the create action it is #school and so on.
That's all.
I hope this helps
it might be a silly question but I am stuck with this for some time as I am new to rails.
I am basically using a custom registration controller to overwrite devise
class RegistrationsController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# GET /Users
def index
#Users = User.all
end
# GET /Users/1
def show
end
# GET /Users/new
def new
#User = User.new
#course = Course.find_by id: params["course_id"]
end
# POST /Users
def create
#User = User.new(user_params)
if #User.save
redirect_to #User.paypal_url(registration_path(#User))
else
render :new
end
end
protect_from_forgery except: [:hook]
def hook
params.permit! # Permit all Paypal input params
status = params[:payment_status]
if status == "Completed"
#User = User.find params[:invoice]
#User.update_attributes notification_params: params, status: status, transaction_id: params[:txn_id], purchased_at: Time.now
end
render nothing: true
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#User = User.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:course_id, :name, :email, :password,:password_confirmation)
end
end
In my routes I have
devise_for :users ,:controllers => { :registrations => "registrations" }
So now I have
edit_user_registration_path GET /users/edit(.:format) registrations#edit
My question is how can I route only edit back to devise/registrations/edit or what can i add to my registrations controller so that I get something similar?
If you want to delegate the create action of the registrations controller to Devise, I recommend you to create a controller that inherits from the Devise one:
class RegistrationsController < Devise::RegistrationsController
def create
super #We call super because we don't want to override this action
end
def edit
#Custom code to override this action
end
end
Your route's configuration stays as it is, you just have to change your controller, you may also want to know that it's possible to ADD functionality to what devise already does, instead of override it:
def edit
super do |resource|
#Here you add what you'll do AFTER devise works
end
end