I'm trying to set up an API that logs in a user to my application and authenticates the user with a token. For this I am creating a new session.
Sessions controller
class Api::V1::SessionsController < Api::V1::BaseController
def create
user = User.where(email: params[:email]).first
if user&.valid_password?(params[:password])
render json: user.as_json(only: [:first_name, :last_name, :email]), status: :created
else
head(:unauthorized)
end
end
Base controller
class Api::V1::BaseController < ActionController::API
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
rescue_from StandardError, with: :internal_server_error
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def internal_server_error(exception)
if Rails.env.development?
response = { type: exception.class.to_s, message: exception.message, backtrace: exception.backtrace }
else
response = { error: "Internal Server Error" }
end
render json: response, status: :internal_server_error
end
def user_not_authorized(exception)
render json: {
error: "Unauthorized #{exception.policy.class.to_s.underscore.camelize}.#{exception.query}"
}, status: :unauthorized
end
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
end
When I test the method, I receive AbstractController::DoubleRenderError, despite the user existing in the database. It points to the last line of the internal_server_error(exception) method of the BaseController.
Can someone help me understand why this code doesn't render the JSON of the user, when the user actually does exist in the database?
Related
I have a Rails API and after starting the server on the first request I am receiving the following error:
Unable to autoload constant Api::V1::UsersController, expected /Users/user/Sites/project/api/app/controllers/api/v1/users_controller.rb to define it
If I then continue to make requests everything loads as normal without errors.
I am using Ruby 2.7.1 and Rails 5.1.7 for just an API application.
I have the following routes defined:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# User Routes
post 'create_user' => 'users#create'
get 'get_user' => 'users#show'
delete 'destroy_user' => 'users#destroy'
put 'update_user' => 'users#update'
# Login Route
post 'login' => 'user_token#create'
end
end
end
My folder structure is
project
- app
- controllers
- api_controller.rb
- application_controller.rb
- Api
- V1
- users_controller.rb
- models
- user.rb
The User's controller it is setup like so (I have also tried inline Api::V1::UsersController inheritance):
module Api
module V1
class UsersController < ApiController
before_action :authenticate_user, only: [:show, :update, :destroy]
def create
#user = User.new(user_params)
if #user.save
render json: { status: 200, data: #user }, status: :ok
else
render json: { status: 422 }, status: :unprocessable_entity
end
end
def show
render json: { status: 200, data: current_user }, status: :ok
end
def update
if current_user.update(user_params)
render json: { status: 200, data: current_user }, status: :ok
else
render json: { status: 422 }, status: :unprocessable_entity
end
end
def destroy
if current_user.delete
render json: { status: 200, data: {} }, status: :ok
else
render json: { status: 422 }, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :first_name, :last_name)
end
end
end
end
Api controller looks like this:
class ApiController < ActionController::API
include Knock::Authenticable
end
Application controller like this:
class ApplicationController < ActionController::API
end
User model looks like this:
class User < ApplicationRecord
has_secure_password
end
Any help would be much appreciated as this structure looks correct to me
I'm getting a weird error in my api rails app:
AbstractController::DoubleRenderError in Api::V1::AdsController#restaurant
Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".
I found this page https://api.rubyonrails.org/classes/ActionController/Base.html about double rendering, but I don't really get what they mean and how it applies to my situation.
I have this in my controller:
class Api::V1::AdsController < Api::V1::BaseController
before_action :set_ad, only: [ :show ]
def index
#ads = policy_scope(Subcategory.find_by(nombre: "Restaurantes").ads)
end
def restaurant
#restaurants = policy_scope(Subcategory.find_by(nombre: "Restaurantes").ads)
end
def show
end
private
def set_ad
#ad = Ad.find(params[:id])
authorize #ad
end
end
And this in my routes:
Rails.application.routes.draw do
devise_for :users
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :ads, only: [ :index, :show ] do
collection do
get 'restaurant', to: 'ads#restaurant'
end
end
end
end
root to: 'pages#home'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
And I have 3 views:
index.json.jbuilder:
json.array! #ads do |ad|
json.extract! ad,
:id,
:empresa,
:direccion_principal,
:tel,
:email_principal,
:web,
:facebook,
:instagram,
:twitter,
:isla
end
restaurant.json.jbuilder = the same as index
show.json.jbuilder:
json.extract! #ad, :id, :empresa, :direccion_principal
Can someone see the problem here?
EDIT:
AdPolicy:
class AdPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
def show?
true
end
end
Base Controller:
class Api::V1::BaseController < ActionController::API
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
rescue_from StandardError, with: :internal_server_error
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def user_not_authorized(exception)
render json: {
error: "Unauthorized #{exception.policy.class.to_s.underscore.camelize}.#{exception.query}"
}, status: :unauthorized
end
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
def internal_server_error(exception)
if Rails.env.development?
response = { type: exception.class.to_s, message: exception.message, backtrace: exception.backtrace }
else
response = { error: "Internal Server Error" }
end
render json: response, status: :internal_server_error
end
end
I found the answer to my own question. It had to do with the BaseController. I forgot to add :restaurant to the after_action :verify_authorized and verify_policy_scoped. With the BaseController below it works now.
class Api::V1::BaseController < ActionController::API
include Pundit
after_action :verify_authorized, except: [:index, :restaurant]
after_action :verify_policy_scoped, only: [:index, :restaurant, :beach_restaurant, :cafe]
rescue_from StandardError, with: :internal_server_error
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def user_not_authorized(exception)
render json: {
error: "Unauthorized #{exception.policy.class.to_s.underscore.camelize}.#{exception.query}"
}, status: :unauthorized
end
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
def internal_server_error(exception)
if Rails.env.development?
response = { type: exception.class.to_s, message: exception.message, backtrace: exception.backtrace }
else
response = { error: "Internal Server Error" }
end
render json: response, status: :internal_server_error
end
end
I have this following controller for my application:
class Api::BaseApiController< ApplicationController
before_action :parse_request, :authenticate_member_from_token!
def index
render nothing: true, status: 200
end
protected
def authenticate_member_from_token!
if !request.headers[:escambo_token]
#member = Member.find_by_valid_token(:activate, request.headers['escambo_token'])
if !#member
render nothing: true, status: :unauthorized
end
end
end
Then, I have another controller that inherits from that Controller:
class Api::CategoryController < Api::BaseApiController
before_action :find_category, except: [:index]
def index
#category = Category.all
puts(#category)
render json: #category
end
But the controller is allowing requests without the token.
EDIT 1: for some reason the index action started to working normally. But still not doing the validation for the token.
EDIT 2: fixing method from private to protected
Your code needs to render :unauthorized if the token is missing, OR invalid. In other words, you need the code to be along the lines of:
def authenticate_member_from_token!
unless Member.find_by_valid_token(:activate, request.headers['escambo_token'])
render nothing: true, status: :unauthorized
end
end
However, with this code you may find yourself double-rendering in the controller. A cleaner approach could be to instead raise an exception, then rescue from it and render appropriately - e.g.
EscamboTokenInvalid = Class.new(StandardError)
rescue_from EscamboTokenInvalid, with: :escambo_unauthorized
def authenticate_member_from_token!
unless Member.find_by_valid_token(:activate, request.headers['escambo_token'])
raise EscamboTokenInvalid
end
end
def escambo_unauthorized
render nothing: true, status: :unauthorized
end
In this post, the errors are rescued in the both api and base controller methods. But it might not be best approach to handle errors because of some reasons are:
Fat Controllers
DRY
Maintainability
In ActionController::Base, we handled ActiveRecord::RecordNotFound in only ApplicationController. But for ActionController::API i have to rescue ActiveRecord::RecordNotFound in every controller. So are there any best approach for handle this problem?
Using Rails 5 and 'active_model_serializers' gem for api
ActionController::API
module Api
module V1
class UsersController < ActionController::API
before_action :find_user, only: :show
def find_user
#user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render json: { error: e.to_s }, status: :not_found
end
end
end
end
ActionController::Base
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found
render file: "#{Rails.root}/public/404", layout: true, status: :not_found
end
end
You can do something like this in application_controller.rb
if Rails.env.production?
rescue_from ActiveRecord::RecordNotFound, with: :render_404
end
def render_404
render json: {meta: meta_response(404, "Record not found")}
end
This would rescue all RecordNotFound exception with 404 but only in production mode.
ActionController::API includes the ActionController::Rescue module which is what provides the rescue_from class method.
I would create an Api::BaseController base class that the Api::V1::UsersController can use instead of using ActionController::API on each controller class. This would allow you have a rescue_from in a single place instead of needing a rescue block on every action.
module Api
class BaseController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
render json: { error: e.to_s }, status: :bad_request
end
end
module V1
class UsersController < BaseController
def find_user
#user = User.find(params[:id])
end
end
end
end
I'd also further create an Api::V1::BaseController to allow for easier versioning of the APIs. Then, if you decide to change the format of the errors for v2, just move the rescue_from in the Api::BaseController to the Api::V1::BaseController, and add a new rescue_from to the new Api::V2::BaseController.
module Api
class CommonBaseController < ActionController::API
# code common across API versions
end
module V1
class BaseController < CommonBaseController
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
render json: { error: e.to_s }, status: :bad_request
end
end
end
module V2
class BaseController < CommonBaseController
# use a custom base error class to make catching errors easier and more standardized
rescue_from BaseError, with: :handle_error
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
status, status_code, code, title, detail =
if e.is_a?(ActiveRecord::RecordNotFound)
[:not_found, '404', '104', 'record not found', 'record not found']
else
[
e.respond_to?(:status) ? e.status : :bad_request,
e.respond_to?(:status_code) ? e.status_code.to_s : '400',
e.respond_to?(:code) ? e.code.to_s : '100',
e.respond_to?(:title) ? e.title : e.to_s,
e.respond_to?(:detail) ? e.detail : e.to_s
]
end
render(
json: {
status: status_code,
code: code,
title: title,
detail: detail
},
status: status
)
end
end
end
end
I'd love to define a rescue_from handler in my controller to respond to the error.
modudle Api
module V1
class TreesController < Api::V1::ApiController
rescue_from TreeNotFound, with: :missing_tree
def show
#tree = find_tree
end
private
def missing_tree(error)
redirect_to(action: :index, flash: error.message)
end
def find_tree
find_forest.trees.find(params[:id])
rescue ActiveRecord::RecordNotFound
raise TreeNotFound, "Couldn't find a tree to hug"
end
end
end
end
However I got some error Api::V1::TreesController::TreeNotFound.
Any idea?
Update
# api_controller.rb
module Api
module V1
class ApiController < JSONAPI::ResourceController
skip_before_action :verify_authenticity_token # Disable CSRF to enable to function as API
respond_to :json
# NOTE: This block is used when you put unrelated values
rescue_from(ArgumentError) do |e|
render json: { error: e.message }, states: 400 # :bad_request
end
rescue_from(ActionController::ParameterMissing) do |e|
error = {}
error[e.param] = ['parameter is required']
response = { errors: [error] }
render json: response, status: 422 # :unprocessable_entity
end
end
end
end
you need to declare the error class first before you can use it. Do this by inheriting from StandardError.
class TreeNotFound < StandardError
end