inherited_resources - best practices for missing parent model - ruby

Maybe you have seen/read the Railscast/Asciicast about subdomains in Rails 3. I'd like you to ask about best practices on how to implement an application behavior when the parent (in this article: "blog") is not found. Let me explain.
blog1.example.com/articles # it's normal situation
example.com/articles # abnormal situation.
In the second example no blog to find, but articles's routes are still available. I know, I can use something like this ...
def rescue_action(exception)
case exception
when ActiveRecord::RecordNotFound
return redirect_to blogs_path, :status => :moved_permanently
end
super
end
... but is it the "Rails way"? Any idea/comment on this?

What I did in this case, was to restrict the routing based on subdomain or no subdomain. In that case, you can easily have routes that only work on subdomains, resulting in a routing error (404) if someone tries to access that same route without a subdomain.
So for example:
routes.rb
Backend::Application.routes.draw do
constraints AppDomainRoutes.new do
# signup paths
get "/signup" => "accounts#new", as: "signup"
post "/signup" => "accounts#create", as: "signup"
# root
root to: "accounts#new"
end
constraints AccountDomainRoutes.new do
# password reset paths
get "/reset_password/:password_reset_token" => "reset_passwords#edit", as: "reset_user_password"
put "/reset_password/:password_reset_token" => "reset_passwords#update", as: "reset_user_password"
# websites
resources :websites
# root
root to: "websites#new"
end
# request password reset paths
get "/reset_password" => "reset_passwords#new", as: "reset_password_request"
post "/reset_password" => "reset_passwords#create", as: "reset_password_request"
# login paths
get "/login" => "sessions#new", as: "login"
post "/login" => "sessions#create", as: "login"
# logout paths
get "/logout" => "sessions#destroy", as: "logout"
delete "/logout" => "sessions#destroy", as: "logout"
end
And then in lib/routes:
app_domain_routes.rb
class AppDomainRoutes
def matches?(request)
request.subdomain.blank? || request.subdomain == "www"
end
end
account_domain_routes.rb
class AccountDomainRoutes
def matches?(request)
request.subdomain.present? && request.subdomain != "www"
end
end
Now, /signup is only accessible from the main application domain www.mydomain.com or mydomain.com and /websites/new is only accessible from *.mydomain.com. But /login is still accessible in both situations, for convenience sake.
Obviously this doesn't solve the issue of visiting invalid.mydomain.com when invalid in fact is not an account in the database.
For this you go back to the application_controller.rb and handle redirection there, like this:
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :redirect_unknown_account
private
# returns current subdomain (account.subdomain) or nil
def account_subdomain
#account_subdomain ||= request.subdomain if request.subdomain.present? && request.subdomain != "www"
end
def current_account
#current_account ||= Account.find_by_username(account_subdomain) if account_subdomain
end
def redirect_unknown_account
if account_subdomain && ! current_account
redirect_to signup_url(host: app_domain), alert: "This account does not exist."
end
end
def account_domain
#account_domain ||= "#{current_account.username}.#{app_domain}" if current_account
end
def app_domain
#app_domain ||= "mydomain.com"
end
end

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

How to fix the problem, When I try authentication on local system its working perfectly, but when uploaded to heroku it comes back with error 500?

I am new to rails and react, this might be a simple one but i cant seem to figure it out.
I am trying to implement a simple jwt authentication using ruby on rails with react as client. I followed the steps that was suggested in :
https://www.pluralsight.com/guides/token-based-authentication-with-ruby-on-rails-5-api
It works as expected on my local system but when i uploaded my app on to heroku it always comes back with error : 500. All the other 'Post' and 'Get' requests work normally. Its only when i try to authenticate and get the auth_token back it runs into 500 error.
this is the request format
post: localhost:3001/api/authenticate
and body:
{
"email": "evin#xyz.com",
"password": "evin"
}
I verified that this data is available on heroku by using get which works perfectly.
I have been working on resolving this for over 2 days now. There is very little information available online on this authentication. There was plenty of recommendations on using auth0. But i could not find much help with this form of authentication.
This is what i have
#Path: /app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action :authenticate_request
attr_reader :current_user
private
def authenticate_request
#current_user = AuthorizeApiRequest.call(request.headers).result
render json: { error: 'Not Authorized' }, status: 401 unless #current_user
end
end
#Path: app/controllers/api/authentication_controller.rb
class Api::AuthenticationController < ApplicationController
skip_before_action :authenticate_request
def authenticate
command = AuthenticateUser.call(params[:email], params[:password])
if command.success?
render json: { auth_token: command.result }
else
render json: { error: command.errors }, status: :unauthorized
end
end
end
#Path: /app/commands/authenticate_user.rb
class AuthenticateUser
prepend SimpleCommand
def initialize(email, password)
#email = email
#password = password
end
def call
JsonWebToken.encode(user_id: user.id) if user
end
private
attr_accessor :email, :password
def user
user = User.find_by_email(email)
return user if user && user.authenticate(password)
errors.add :user_authentication, 'invalid credentials'
nil
end
end
#Path: /app/commands/authorize_api_request.rb
class AuthorizeApiRequest
prepend SimpleCommand
def initialize(headers = {})
#headers = headers
end
def call
user
end
private
attr_reader :headers
def user
#user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
#user || errors.add(:token, 'Invalid token') && nil
end
def decoded_auth_token
#decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
else
errors.add(:token, 'Missing token')
end
nil
end
end
#Path: /lib/json_web_token.rb
class JsonWebToken
class << self
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def decode(token)
body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
HashWithIndifferentAccess.new body
rescue
nil
end
end
end
#path: /config/application.rb
require_relative 'boot'
require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
# require "sprockets/railtie"
require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Deveycon
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
#Autoload lib for encrypt and decrypt
config.autoload_paths << Rails.root.join('lib')
# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
end
end
I had similar issues, the API works perfectly on localhost after uploading to Heroku, I still got unauthorized on secure pages even with the token on the headers.
I added
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
to config/secrets.yml
Please check the more details log of your heroku application by using Heroku CLI.
heroku logs -t
If the problem with AuthenticateUser::JsonWebToken use auto loaded in your
config/application.rb
class Application < Rails::Application
#.....
config.autoload_paths << Rails.root.join('lib')
#.....
end
I hope that helpful to resolve your issue.
In #lib/JsonWebToken:
Just increase the exp time of token and replace .secrets.secret_key_base with
.credentials.read
class JsonWebToken
class << self
def encode(payload, exp = 1200.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, Rails.application.credentials.read)
end
def decode(token)
body = JWT.decode(token, Rails.application.credentials.read)[0]
HashWithIndifferentAccess.new body
rescue
nil
end
end
end

Hanami parameters whitelisting

Following the hanami docs, in order to block a admin parameter inside an action, I can use the following configuration:
params do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
def call(params)
puts params[:email] # => "alice#example.org"
puts params[:address][:country] # => "Italy"
puts params[:admin] # => nil
end
But this does not work for nested parameters, i.e.:
params do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
def call(params)
puts params[:email] # => "alice#example.org"
puts params[:address] # => { country: "Italy", admin: true }
puts params[:address][:admin] # => true
end
I was able to solve this by using select to filter out the undesirable parameters with a private method, but this does not seems like the Hanami way. What would be the proper way to do this whitelisting of nested parameters?
I have never had this issue when using Hanami Validations. Within the app directory there should be a validations folder which should have the same directory structure as your controllers, views, templates etc. Your validation file should look something like this:
# apps/web/validations/users/create.rb
module Web
module Validations
module Users
class Create < Web::Action::Params
predicates Web::Validations::CommonPredicates
validations do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
end
end
end
end
And then your controller should set the params to be filtered through the validation:
module Web
module Controllers
module Users
class Create
include Web::Action
params Web::Validations::Users::Create
def call(params); end
end
end
end
end

Facebook Graph API for websites using Ruby Koala gem in Sinatra

I want to implement Facebook login for web apps. All I need is the basic public information of a user for the account creation steps.
This is what I have done:
Created a basic Facebook app with nearly no custom permissions.
Used the APP_ID and APP_SECRET in Koala to get access_token.
Everything worked perfectly, I am able to login/logout.
Just that the only information I am able to get back when I do: graph.get_object('me') is the logged in user's name and an id (It doesn't look like the default Facebook id).
Surprised whether something changed in the new API, I tested the gem in the console using the access_token from graph explorer (where all permissions are enabled by default). And I get all data using the same method call.
When I review what all the app gets while signing up; I see that the user's basic information, profile pic and other public data will be accessible to the app.
Any idea why this is so? It seems I am missing something obvious. The code is available in Github. But this is pretty much everything to it:
require 'bundler'
Bundler.require :default
Dotenv.load '.env'
require_relative './app/constants.rb'
module Banana
class App < Sinatra::Base
use Rack::Session::Cookie, secret: COOKIE_SECRET
set :public_folder, File.dirname(__FILE__) + '/bower_components'
get '/' do
if logged_in?
haml :welcome_in, layout: :layout
else
haml :log_in, layout: :layout
end
end
get '/log_out' do
session['oauth'] = nil
session['access_token'] = nil
redirect '/'
end
get '/log_in' do
session['oauth'] = Koala::Facebook::OAuth.new(APP_ID, APP_SECRET, "#{request.base_url}/call_back")
redirect session['oauth'].url_for_oauth_code()
end
get '/call_back' do
begin
session['access_token'] = session['oauth'].get_access_token(params[:code])
rescue
redirect '/?error=user_denied'
end
redirect '/'
end
get '/test' do
if logged_in?
p graph.get_object("rakeshbs")
"e"
else
redirect '/'
end
end
def logged_in?
!session['access_token'].nil?
end
def toggle_access
logged_in? ? '/log_out' : '/log_in'
end
def graph
#graph ||= Koala::Facebook::API.new(session['access_token'])
end
def errored?
!params["error"].nil?
end
def user
p graph.get_connections(:me, :photos) # This is just nil
#user ||= OpenStruct.new(
name: graph.get_object("me")["name"], # All I get here is just a hash with the name and an id!
photo: 'http://semantic-ui.com/images/avatar/small/elliot.jpg'
)
end
end
end
You should add fields parameter.
Something like this:
graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday,photos,email' })

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.

Resources