If my Sinatra Application structure is like this. Copied from Sinatra Help
require 'sinatra/base'
class LoginScreen Sinatra::Base
enable :sessions
get('/login') { haml :login }
post('/login') do
if params[:name] == 'admin' && params[:password] == 'admin'
session['user_name'] = params[:name]
else
redirect '/login'
end
end
end
class MyApp Sinatra::Base
# middleware will run before filters
use LoginScreen
before do
unless session['user_name']
halt "Access denied, please login."
end
end
get('/') { "Hello #{session['user_name']}." }
end
The question would be if the two applications require the same configuration such as, helpers, registers and asset-pack. How do I make it consistent between the two application without duplicating the code. Could I do something like this?
require 'sinatra/base'
class LoginScreen Sinatra::Base
include_relative("config_file.rb")
enable :sessions
get('/login') { haml :login }
post('/login') do
if params[:name] == 'admin' && params[:password] == 'admin'
session['user_name'] = params[:name]
else
redirect '/login'
end
end
end
class MyApp Sinatra::Base
# middleware will run before filters
include_relative("config_file.rb")
use LoginScreen
before do
unless session['user_name']
halt "Access denied, please login."
end
end
get('/') { "Hello #{session['user_name']}." }
end
config_file.rb
helpers Sinatra::Helper1
helpers Sinatra::Helper2
helpers Sinatra::Helper3
register Sinatra1
register Sinatra2
register Sinatra3
How about using inheritance?
class MyBase < Sinatra::Base
configure do
helpers Sinatra::Helper1
register Sinatra1
end
end
class LoginScreen < MyBase
end
class MyApp < MyBase
use LoginScreen
end
This way configuration gets shared among all applications that descend from MyBase.
Related
I have a simple Sinatra app with two controllers and api helper
# ApplicationController
class ApplicationController < Sinatra::Base
register Sinatra::ActiveRecordExtension
helpers ApiHelper
configure :production, :development do
enable :logging
end
before do
content_type :json
end
get '/hello' do
{ message: 'Hello!' }.to_json
end
end
# ArticlesController
class ArticlesController < ApplicationController
before do
authenticate!
end
get '/articles' do
articles = Article.all
articles.map { |article| serialize(article) }
end
...
end
# ApiHelper
module ApiHelper
def authenticate!
halt 403, { message: 'Unauthorized!' }.to_json unless authorized?
end
private
def authorized?
request.env['HTTP_AUTHORIZATION'] == 'Bearer 123qweasd'
end
end
When I do
curl -X GET -i -H 'Accept: application/json' http://localhost:4567/hello
to do helth check I get 403 Unauthorized. Why? I don't require authentication in /hello endpoint, only in /articles CRUD endpoints so I don't understand why it authenticates in /hello. According to the docs before block is used to perform some action before other action runs but I don't call authenticate! in before block in ApplicationController. What am I missing?
It turned out that not knowingly I was using ArticlesController as a middleware. My config.ru looked like this.
run ApplicationController
use ArticlesController
which made it so that the authenticate! was called before every request.
I changed my config.ru to this:
map '/' do
run ApplicationController
end
map '/articles' do
run ArticlesController
end
And it works.
I've used this series as a starting point for a Rails backend for a work portfolio website. Adapting it has been mostly straightforward, and it's doing what I want it to. The one big problem is that the 'index' and 'show' (read actions) should be available without authentication, while 'create', 'update', and 'delete' (write actions) should require a valid JWT.
Following the approach used to exclude the signup and login routes from authentication, I've tried
skip_before_action :authorize_request, only: [:index, :show]
in the appropriate controller. This will however crash the application, with
NoMethodError (undefined method `works' for nil:NilClass):
app/controllers/works_controller.rb:10:in `index'
While the problem seems apparent - if skipping the authentication action the class doesn't get instantiated - the fix isn't, to me at least. Could anyone please help?
The code for the project is here.
Application controller
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
# called before every action on controllers
before_action :authorize_request
attr_reader :current_user
private
# Check for valid request token and return user
def authorize_request
#current_user = (AuthorizeApiRequest.new(request.headers).call)[:user]
end
end
'Works' controller
class WorksController < ApplicationController
#skip_before_action :authorize_request, only: [:index, :show]
before_action :set_work, only: [:show, :update, :destroy]
# GET /works
def index
#works = current_user.works
json_response(#works)
end
# POST /works
def create
#work = current_user.works.create!(work_params)
json_response(#work, :created)
end
# GET /works/:id
def show
json_response(#work)
end
# PUT /works/:id
def update
#work.update(work_params)
head :no_content
end
# DELETE /works/:id
def destroy
#work.destroy
head :no_content
end
private
def work_params
# whitelist params
params.permit(:title, :nature, :role, :client, :timeframe, :description, :images, :url, :blog_post)
end
def set_work
#work = Work.find(params[:id])
end
end
'Users' controller
class UsersController < ApplicationController
skip_before_action :authorize_request, only: :create
def create
user = User.create!(user_params)
auth_token = AuthenticateUser.new(user.username, user.password).call
response = { message: Message.account_created, access_token: auth_token }
json_response(response, :created)
end
def show
json_response(username: current_user.username)
end
private
def user_params
params.permit(
:username,
:password,
:password_confirmation
)
end
end
'Authentication' controller
class AuthenticationController < ApplicationController
skip_before_action :authorize_request, only: :authenticate
# return auth token once user is authenticated
def authenticate
auth_token =
AuthenticateUser.new(auth_params[:username], auth_params[:password]).call
json_response(access_token: auth_token)
end
private
def auth_params
params.permit(:username, :password)
end
end
'AuthenticateUser' helper
class AuthenticateUser
def initialize(username, password)
#username = username
#password = password
end
# Service entry point
def call
JsonWebToken.encode(user_id: user.id) if user
end
private
attr_reader :username, :password
# verify user credentials
def user
user = User.find_by(username: username)
return user if user && user.authenticate(password)
# raise Authentication error if credentials are invalid
raise(ExceptionHandler::AuthenticationError, Message.invalid_credentials)
end
end
'AuthorizeApiRequest' helper
class AuthorizeApiRequest
def initialize(headers = {})
#headers = headers
end
# Service entry point - return valid user object
def call
{
user: user
}
end
private
attr_reader :headers
def user
# check if user is in the database
# memoize user object
#user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
# handle user not found
rescue ActiveRecord::RecordNotFound => e
# raise custom error
raise(
ExceptionHandler::InvalidToken,
("#{Message.invalid_token} #{e.message}")
)
end
# decode authentication token
def decoded_auth_token
#decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end
# check for token in `Authorization` header
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
end
raise(ExceptionHandler::MissingToken, Message.missing_token)
end
end
'ExceptionHandler' helper
module ExceptionHandler
extend ActiveSupport::Concern
# Define custom error subclasses - rescue catches `StandardErrors`
class AuthenticationError < StandardError; end
class MissingToken < StandardError; end
class InvalidToken < StandardError; end
included do
# Define custom handlers
rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two
rescue_from ExceptionHandler::AuthenticationError, with: :unauthorized_request
rescue_from ExceptionHandler::MissingToken, with: :four_twenty_two
rescue_from ExceptionHandler::InvalidToken, with: :four_twenty_two
rescue_from ActiveRecord::RecordNotFound do |e|
json_response({ message: e.message }, :not_found)
end
end
private
# JSON response with message; Status code 422 - unprocessable entity
def four_twenty_two(e)
json_response({ message: e.message }, :unprocessable_entity)
end
# JSON response with message; Status code 401 - Unauthorized
def unauthorized_request(e)
json_response({ message: e.message }, :unauthorized)
end
end
The error message states:
NoMethodError (undefined method `works' for nil:NilClass):
app/controllers/works_controller.rb:10:in `index'
Or to translate that, on line 10 of the works_controller.rb file, we're calling a method called works on nil, which is throwing an error.
Assuming line 10 of the works_controller is
#works = current_user.works
Then the error message is telling us that we're calling works on nil, i.e. we have no current_user.
Either where you assign this code is not working properly, or you're accessing this part of the code without signing in and haven't coded around that. Either way, the current_user variable is returning nil and shouldn't be.
I am using Sinatra to build a small Ruby API, and I would like to get some of the errors and configurations set to work at a global level so that i don't need to set them at the start of each of the classes.
My structure is this:
content_api.rb
require 'sinatra/base'
require 'sinatra/namespace'
require 'sinatra/json'
require 'service_dependencies'
require 'api_helpers'
require 'json'
module ApiApp
class ContentApi < Sinatra::Base
helpers Sinatra::JSON
helpers ApiApp::ApiHelpers
include ApiApp::ServiceDependencies
before do
content_type :json
end
get '/' do
content = content_service.get_all_content
content.to_json
end
get '/audio' do
package =content_service.get_type 'Audio'
package.to_json
end
get '/video' do
package =content_service.get_type 'Video'
package.to_json
end
get '/document' do
package =content_service.get_type 'Document'
package.to_json
end
end
end
config.ru:
$LOAD_PATH.unshift *Dir[File.join(File.dirname(__FILE__), '/src/**')]
$LOAD_PATH.unshift *Dir[File.join(File.dirname(__FILE__), '/src/api/**')]
require 'content_api'
require 'package_api'
require 'utility_api'
require 'sinatra/base'
configure do
set :show_exceptions => false
end
error { |err| Rack::Response.new([{'error' => err.message}.to_json], 500, {'Content-type' => 'application/json'}).finish }
Rack::Mount::RouteSet.new do |set|
set.add_route ApiApp::ContentApi, {:path_info => %r{^/catalogue*}}, {}, :catalogue
set.add_route ApiApp::PackageApi, {:path_info => %r{^/package*}}, {}, :package
set.add_route ApiApp::UtilityApi, {:path_info => %r{^/health_check*}}, {}, :health_check
end
When I run this, it will run okay, but when I force a 500 error (shut down MongoDb) I get a standard html type error that states:
<p id="explanation">You're seeing this error because you have
enabled the <code>show_exceptions</code> setting.</p>
If I add the
configure do
set :show_exceptions => false
end
error { |err| Rack::Response.new([{'error' => err.message}.to_json], 500, {'Content-type' => 'application/json'}).finish }
inside the content_api.rb file, then I get a JSON error returned as I would like to receive.
however, as I am building this modular, I don't want to be repeating myself at the top of each class.
Is there a simple way to make this work?
The simplest way to do this would be to reopen Sinatra::Base and add the code there:
class Sinatra::Base
set :show_exceptions => false
error { |err|
Rack::Response.new(
[{'error' => err.message}.to_json],
500,
{'Content-type' => 'application/json'}
).finish
}
end
This is probably not advisable though, and will cause problems if your app includes other non-json modules.
A better solution might be to create a Sinatra extension that you could use in your modules.
module JsonExceptions
def self.registered(app)
app.set :show_exceptions => false
app.error { |err|
Rack::Response.new(
[{'error' => err.message}.to_json],
500,
{'Content-type' => 'application/json'}
).finish
}
end
end
You would then use it by registering it in your modules:
# require the file where it is defined first
class ContentApi < Sinatra::Base
register JsonExceptions
# ... as before
end
As an alternative, inheritance:
class JsonErrorController < Sinatra::Base
configure do
set :show_exceptions => false
end
error { |err| Rack::Response.new([{'error' => err.message}.to_json], 500, {'Content-type' => 'application/json'}).finish }
end
class ContentApi < JsonErrorController
# rest of code follows…
end
From Sinatra Up and Running p73:
Not only settings, but every aspect of a Sinatra class will be
inherited by its subclasses. This includes defined routes, all the
error handlers, extensions, middleware, and so on.
I am using Devise 1.4.2, RSpec 2.6.0 and Rails 3.1.0.rc6. My routes.rb looks like this:
scope "(:locale)", :locale => /e(s|n)/ do
resources :demotivideos, :only => [:index, :show]
devise_for :users
namespace "admin" do
resources :demotivideos, :except => [:index, :show]
end
end
I am spec'ing that, when a not logged in user acces new, create or update, he should be redirected to new_user_session_path. For this, I am using the following code
context "when not logged in" do
before(:each) do
sign_out user
end
describe "GET new" do
it "should redirect to new user session" do
get :new
response.should redirect_to(new_user_session_path)
end
end
describe "POST create" do
it "should redirect to new user session" do
post :create, :demotivideo => valid_attributes
response.should redirect_to(new_user_session_path)
end
end
describe "PUT update" do
it "should redirect to new user session" do
put :update, :id => 1, :demotivideo => valid_attributes
response.should redirect_to(new_user_session_path)
end
end
end
All are failing because of the same reason: expected route includes the locale (by default en) but the actual redirect was to the same path without locale. My application controller was modified as told in Rails Guides:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_locale
def default_url_options(options={})
logger.debug "default_url_options is passed options: #{options.inspect}\n"
{ :locale => I18n.locale }
end
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
end
What am I doing wrong?
Seems like though Rails Guides uses def default_url_options in Devise you need def self.default_url_options. Don't know the difference, though.
I'm using the casrack-the-authenticator gem for CAS authentication. My server is running Thin on top of Sinatra. I've gotten the CAS authentication bit working, but I'm not sure how to tell Rack to intercept "/index.html" requests to confirm the CAS login, and if the user is not allowed to view the page, return a HTTP 403 response instead of serving the actual page. Does anyone have experience with this? Thanks.
My app:
class Foo < Sinatra::Base
enable :sessions
set :public, "public"
use CasrackTheAuthenticator::Simple, :cas_server => "https://my.cas_server.com"
use CasrackTheAuthenticator::RequireCAS
get '/' do
puts "Hello World"
end
end
My rackup file:
require 'foo'
use Rack::CommonLogger
use Rack::Lint
run Foo
Initial attempt at getting Rack to understand authentication in its file service (comments and thoughts welcome):
builder = Rack::Builder.new do
map '/foo/index.html' do
run Proc.new { |env|
user = Rack::Request.new(env).session[CasrackTheAuthenticator::USERNAME_PARAM]
[401, { "Content-Type" => "text/html" }, "CAS Authentication Required"] unless user
# Serve index.html because we detected user
}
end
map '/foo' do
run Foo
end
end
run builder
Casrack-the-Authenticator will put the CAS information into the Rack session. You can pull that out in another piece of Rack middleware or in your Sinatra app.
The following is for a Rails application, but the concept is similar for Sinatra or a Rack middleware:
# in app/controllers/application_controller.rb:
protected
def require_sign_in!
render :nothing => true, :status => 403 unless signed_in?
end
def signed_in?
current_user.present?
end
def current_user
#current_user ||= Person.find_by_username(session[CasrackTheAuthenticator::USERNAME_PARAM])
end