I am connecting to Shopify's API with the gem omniauth-shopify-oauth2.
In config/initializers/shopify.rb I am setting my OmniAuth::Builder like this:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :shopify, "my_shopify_api_key", "my_shopify_secret",
scope: 'read_orders',
setup: lambda { |env| params = Rack::Utils.parse_query(env['QUERY_STRING'])
env['omniauth.strategy'].options[:client_options][:site] = "http://#{params['shop']}" }
end
Then from any controller file I can access Shopify's API like this:
omniauth = request.env['omniauth.auth']
if omniauth && omniauth[:provider] && omniauth[:provider] == "shopify"
token = omniauth['credentials'].token
session = ShopifyAPI::Session.new("#{username}.myshopify.com", token)
ShopifyAPI::Base.activate_session(session)
# Do stuff with the session
ShopifyAPI::Base.clear_session
end
If, on the other hand, I move the controller functionality to e.g. app/my_custom_folder/my_custom_file.rb I get an error in the very first line of code:
NameError (undefined local variable or method `request' for #<MyCustomClass:0x007fe07ec11f68>):
I assume that the error is raised because my custom classes and files can't access the middleware functionality. So how can I extend the functionality?
Rails auto-requires everything in certain directories, but my_custom_folder isn't one of those.
You could add something like this to config/application.rb
config.autoload_paths += %W(
#{config.root}/my_custom_folder
)
Or, you could drop my_custom_file.rb in a blessed location like app/controllers/concerns
ALSO:
It seems to me that the code you moved to my_custom_file.rb probably also expects to be inside a controller, which has access to request (through ActionController::Base).
Without knowing what my_custom_file.rb looks like, I would propose you set it up as an include like so:
# in app/controllers/concerns/my_custom_file.rb
module MyCustomFile
extend ActiveSupport::Concern
included do
# if necessary to run stuff on include:
# before_action :do_some_oauth_shopify_stuff
end
# This will be treated as part of all controllers now:
def do_some_oauth_shopify_stuff
# your code
end
end
# near top of app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include MyCustomFile
# rest of application_controller.rb
end
And maybe pick a better name for the module/file.
Related
I'm using engines inside my Rails 7 app. I've got Core engine where I can find a devise mailer (from devise_invitable gem). Think there is only 1 Devise application mailer possible, and I don't need a copy anything inside devise.rb.
My goal is to to override invitation_instructions method from Core engine into PaymentProcess engine. To do that I'll use concerns, so in a nutshell:
add a devise mailer concern inside payment_engine and include it to devise mailer inside payment_process/engine.rb initializer
overwrite invitation_instructions, but keep super when a user is not a payment_process user.
Here is my code:
engines/core/app/mailers/devise_application_mailer.rb
class DeviseApplicationMailer < Devise::Mailer
def invitation_instructions(recipient, *args)
account, = recipient.accounts
scope = 'devise.mailer.invitation_instructions'
#invited_by_text = invited_by_text(account, recipient.invited_by, scope)
mail = super
mail.subject = I18n.t('subject_with_account', account_name: account.name, scope:) if account.present?
mail
end
# other stuff (...)
end
engines/payment_process/app/mailers/concerns/devise_application_mailer_concern.rb:
module DeviseApplicationMailerConcern
extend ActiveSupport::Concern
included do
def invitation_instructions(resource, *args)
if resource.is_a?(PaymentProcess)
recipient, customer = resource
scope = 'devise.mailer.invitation_instructions'
#invited_by_text = invited_by_text(customer, recipient.invited_by, scope)
mail = super
mail.subject = I18n.t('subject', scope:) if customer.present?
mail
else
super
end
end
end
end
So now I want to include everything inside engine initializer:
engines/payment_process/lib/payment_process/engine.rb
module PaymentProcess
class Engine < ::Rails::Engine
engine_name 'payment_process'
isolate_namespace PaymentProcess
# some other stuff
config.to_prepare do
# some other stuff
Devise::Mailer.include(PaymentProcess::DeviseApplicationMailerConcern)
end
end
end
And all of these produces me an error of:
block in class:Engine': uninitialized constant PaymentProcess::DeviseApplicationMailerConcern (NameError)
Devise::Mailer.include(PaymentProcess::DeviseApplicationMailerConcern)
I've tried to require file path inside the engine but it won't help. I also tried to include concern directly inside the engines/core/app/mailers/devise_application_mailer.rb but I'm getting the same error.
What did I missed? or maybe it's not possible with to include anything to Devise ?
In phoenix framework with pipeline we can enable specify middlewares for some route, for example:
defmodule HelloPhoenix.Router do
use HelloPhoenix.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloPhoenix do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
end
scope "/api", HelloPhoenix do
pipe_through :api
end
end
if request from /api, will only trigger plug :accepts, ["json"] middleware
if request from /, will trigger session, flash, ...etc middlewares
how to achieve this on rails, If I am using grape for build api and rails for build web page and enable difference middleware for each other?
Unlike Phoenix applications, you cannot (easily) change what middleware is used for a request within Rails. The best way to get this kind of behaviour in a Rails application would be to make the controllers for a particular route inherit from a common base controller, and then define the behaviour for these particular controllers in that base controller.
Using the above example of the /api routes going through different "middleware", you can have a controller like this:
module API
class BaseController < ActionController::API
# things unique to API routes go here
end
end
Inside this controller, you could write some before_action callbacks that ensure things like:
Inbound requests are JSON only
Requests are authenticated with a particular token
Then you can inherit from this controller for all API controllers:
module API
class PostsController < API::BaseController
end
end
With your regular controllers you can do all the regular things:
class ApplicationController < ActionController::Base
# things unique to non-api routes go here
end
And then:
class PostsController < ApplicationController
end
You could of course segment it more; maybe you might have another route like /accounts/1/posts where you have to be authenticated as the user of that account to see the posts. You can take the same approach:
module Accounts
class BaseController < ActionController::Base
# find account, check if the user is authenticated, etc.
end
end
And:
module Accounts
class PostsController < Accounts::BaseController
end
end
So in summary: Rails doesn't let you do this on a routing level (easily), but you can accomplish the same thing at the controller level.
The solution I was looking for was something more in this direction:
application.rb:
# after Bundler.require(...)
require_relative '../lib/engines/website/lib/website'
lib/engines/website/lib/website.rb:
require_relative "website/engine"
module Website; end
lib/engines/website/lib/website/engine.rb:
module Website
class Engine < ::Rails::Engine
middleware.use ActionDispatch::Cookies
middleware.use ActionDispatch::Session::CookieStore
middleware.use ActionDispatch::Flash
end
end
config/routes.rb:
mount Website::Engine => "/website"
And everything for the website goes in the typical directory structure under the engine directory:
lib
engines
website
app
assets
...
controllers
...
views
...
config
routes.rb
lib
website
website.rb
Reference: Build 2 middleware stacks in Rails app
I'm pretty sure that you can achieve this result by configuring Rack middleware stack, but it won't be configurable in a rails application.
Sometimes it's affordable to have unnecessary middleware for some routes.
Almost all apps that I saw are using same middleware for every request.
And yes, Phoenix behavior is better here, Rails behavior done in earliest versions, so it's hard to change something now.
I have to make a rails API only i.e. input is POST request and output will be an JSON response. I have to interact with mysql database with my own table names i.e. mysql tables are already created.
Below is the folder structure with "helpers" even though we are not using any "views". We are accessing the helper methods from our controllers. Please confirm if I am correct or not. Thanks in advance.
1) app/controllers/application_controller.rb
class ApplicationController < ActionController::API
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
2) app/controllers/feature_management_controller.rb
class FeatureManagementController < ApplicationController
def populate_bean
#json = OrionCountryList.new.sample_function
end
def send_response(bean)
helper = FeatureManagementHelper.new
if (bean.method.eql?"get_feature_list") && (!bean.app_key.blank?) && (bean.app_key!=nil) && (bean.app_key.casecmp("NULL")!=0)
logger.info bean.print_bean "Request for fetching featureList by app_key : " + bean.app_key.to_s + " And userID: " + bean.user_id.to_s
##json_response = helper.get_feature_list bean
else
logger.error "METHOD NOT FOUND. method during feature management :"+bean.method+" app_key :"+bean.app_key
##json_response = {:message => "API not avaliable"}
end
logger.info("Final json_response sent to app : "+##json_response.to_json)
render :json => ##json_response
end
end
3) app/helpers/application_helper.rb
class ApplicationHelper
APP_CONFIG = YAML.load(File.read(File.expand_path('../../../config/app_config.yml', __FILE__)))
end
4) app/helpers/feature/feature_management_helper.rb
class FeatureManagementHelper
def get_feature_list(bean)
response = Hash.new
response = {:success_code => "1"}
return response
end
end
Here we are using "class" key word inside the helpers. But on searching, it seems "module" key word is needed. But we couldn't find the way to access module methods of helpers inside controllers.
Any help is appreciated.Thanks!!!
UPDATE
#Ekkerhard, Thanks for the suggestion,
I have refrained from using helpers in the above way mentioned and instead used PORO for implementing my business logic as suggested by #spikermann using
this_link
Upon implementing the changes, my code structure looks something like this:
1) app/controllers/feature_management_controller/feature_management.rb
class FeatureManagementController
class FeatureManagement
def get_feature_list(bean)
response = Hash.new
response = {:success_code => "1"}
return response
end
end
end
Similarly for any controller "test_controller" I have a folder named "test_controller" at the location /app/controllers/
and I am keeping the business logic inside a test.rb file inside this "test_controller" folder.
2) We have all the controllers inside the /app/controllers
3) We have all the models inside the /app/models
4) We are reading the configuration file inside /config/application.rb
class Application < Rails::Application
config.autoload_paths += Dir["#{config.root}/lib/**/"]
APP_CONFIG = YAML.load(File.read(File.expand_path('app_config.yml', __FILE__)))
config.time_zone = "New Delhi"
config.active_record.default_timezone = :local
config.autoload_paths += Dir["#{config.root}/app/**/"]
end
Though if I read the config file from the feature_management.rb file things are working just fine i.e. adding this line to the feature_management.rb file :
/app/controllers/feature_management_controller/feature_management.rb
APP_CONFIG = YAML.load(File.read(File.expand_path('../../../../config/app_config.yml',
__FILE__)))
but upon trying to read the configuration from the application.rb file I am getting an error :
NameError (uninitialized constant FeatureManagementController::FeatureManagement::APP_CONFIG):
I was wondering whether this is the correct way to proceed and is there a better way to do it.
Appreciate your inputs..!!!
If you need, you can include the module inside your controller and access the methods. Like:
include HelperModuleName
Frankly, to me it seems that you are trying to just "get by" with Rails here and are using other paradigms in a Rails environment. I don't think you're going to be happy with that in the long (or even short) term; if you use Rails that way it will just get in your way.
First of all, I would not use helpers in this way, at all, ever. In my opinion, helpers are there solely for cutting out "stupid" code from view templates, to cut down on "programming" clutter inside otherwise HTML'ish/JSON'ish templates. And they are definitely never domain methods. They do domain-agnostic stuff like render form elements, complex tables or things like that.
Just because you are not outputting HTML but JSON does not mean you have to ditch the MVC paradigm completely, like you are doing. There are JSON templates, see In Rails, how do you render JSON using a view? .
You are using a helper to load configuration. Configuration lives in config/application.rb, config/environments/*.rb, config/initializers/*.rb etc. - see http://guides.rubyonrails.org/configuring.html . You should never need to load the YAML in a helper.
Your controller code suggests that you do not use routes.rb to structure your requests. Branching like you have in your controller is a big smell for me. See http://guides.rubyonrails.org/routing.html .
Maybe not the answers you are looking for, but that's my 2€. :)
Been fighting with this for a while now, not sure why it isn't working.
The gist is looking to use Devise with LDAP. I don't need to do anything except for authenticate so I have no need to use anything except for a custom strategy.
I created one based on https://github.com/plataformatec/devise/wiki/How-To:-Authenticate-via-LDAP and as far as I can tell everything should work except whenever I try to run the server (or rake route) I get a NameError
lib/devise/models.rb:88:in `const_get': uninitialized constant Devise::Models::LdapAuthenticatable (NameError)
I've traced the error down to my app/models/user.rb
class User < ActiveRecord::Base
devise :ldap_authenticatable, :rememberable, :trackable, :timeoutable
end
If I remove :ldap_authenticatable then the crash goes away but I have no routes to user#session and a login prompt cannot be accessed.
My supporting files:
lib/ldap_authenticatable.rb
require 'net/ldap'
require 'devise/strategies/authenticatable'
module Devise
module Strategies
class LdapAuthenticatable < Authenticatable
def authenticate!
if params[:user]
ldap = Net::LDAP.new
ldap.host = 'redacted'
ldap.port = 389
ldap.auth login, password
if ldap.bind
user = User.where(login: login).first_or_create do |user|
success!(user)
else
fail(:invalid_login)
end
end
end
def login
params[:user][:login]
end
def password
params[:user][:password]
end
end
end
end
Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable)
And finally, inside config/initializers/devise.rb
Devise.setup do |config|
# ==> LDAP Configuration
require 'ldap_authenticatable'
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :ldap_authenticatable
end
end
I've exhausted my searches, maybe someone can see something I am missing.
Cheers
Is your lib/ldap_authenticatable.rb is in the autoload path or explicitly required? Since Rails 3 code in lib folder isn't autoloaded by default any more. Here is one way on how to solve it
IMHO Devise is a great gem. However in order to write your own strategy you have to be familiar not only with Devise but with Warden source code as well, and a lot of boilerplate code needs to be written in various places, so I start to investigate how to make customizations easier for Devise and come up with this gem devise_custom_authenticatable. You can check it and probably it'll solve your problem in a different way. This gem is used in production code base for rather busy application, so it battle proven :)
File paths should match namespaces. You need to add 2 levels of directories.
mkdir lib/devise
mkdir lib/devise/strategies
mv lib/ldap_authenticatable.rb lib/devise/strategies/ldap_authenticatable.rb
Since you are namespaced to
module Devise
module Strategies
class LdapAuthenticatable < Authenticatable
...
few steps to take care while creating custom strategy:
you will have to take care of the strategies folder as #csi mentioned along with it create a models folder and inside models create ldap_authenticatable.rb . so the structure will look like this.
lib/devise/strategies/ldap_authenticatable.rb
lib/devise/models/ldap_authenticatable.rb
Add these lines to lib/devise/models/ldap_authenticatable.rb
require Rails.root.join('lib/devise/strategies/ldap_authenticatable')
module Devise
module Models
module LdapAuthenticatable
extend ActiveSupport::Concern
end
end
end
In config/initializers/devise.rb add these lines to the top.
Devise.add_module(:ldap_authenticatable, {
strategy: true,
controller: :sessions,
model: 'devise/models/ldap_authenticatable',
route: :session
})
This should take care of the custom auth.
If I do:
rails generate scaffold account/user username
I get a controller that looks like this:
class Account::UsersController < ApplicationController
def index
#account_users = Account::User.all
end
...
end
If I include the Account Module, then it looks like all the database calls don't need to be prefixed with "Account::". I.e.
class Account::UsersController < ApplicationController
include Account
def index
#account_users = User.all #this works because I included the Account Module above
end
...
end
Now if I were to move my
controllers/account/users_controller.rb
file to:
controllers/admin/account/users_controller.rb
The file looks like this (note: I also corrected my routes file after this move):
class Admin::Account::UsersController < ApplicationController
include Account
def index
#account_users = User.all #this call does not work now
end
...
end
But I get an error saying "uninitialized constant Admin::Account::UsersController::User"
It looks like rails is trying to make a database call on the "User" model without the "Account::" module in front of it.
So how does including modules in controllers work? Why does this not work when I move my controller into a different file (and leave the model in the same location from the generated scaffold) but it works with the scaffold generated files? How can I fix this issue?
Resolving the name of a module is done relative to the current module. Try and change it to:
include ::Account
or
include ::Admin::Account
(depending on the module in which your User model is defined)
This will tell ruby to look in the global namespace for the module Account
I guess I didn't realize you can just explicitly require the path to the module you would like to include. I learned this after reading up on modules some more...
So adding an explicit call to "require 'account/user'" just outside the controller class makes it so including the module in the controller works.