I'm creating a really simple Rails application with one specific purpose: add a Calendar event to a Google Calendar account.
I'm following the Ruby Guide from the Google Calendar API.
I was able to run the provided testing code (Ruby only, no framework) but I'm having a hard time accessing the credentials from a Rails project and I'm not sure the proper ("idiomatic"?) way to do it and organize the project.
Part of the process is using OAuth 2.0 since this goal requires access to Google User's private data (both read and write), I'm following the Using OAuth 2.0 for Web Services instructions.
Right now I have several different questions regarding best practices and/or the proper way to organize code:
Google provides a client_secret.json that have the credentials to access the application. Where should I keep it? Should I keep it in a .env file in the Development environment and (in my case) in Heroku's ENV VARS in the Production Environment?
I tried keeping the client_secret.json file in the project's root folder (same path as the Gemfile), added it to the .gitignore but I wasn't able to require "#{Rails.root}/client_secret.json":
/Users/jsoifer/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/dependencies.rb:293:in `require': No such file to load -- /Users/jsoifer/Developer/Tiny-Things/tiny-booking/client_secret.json (LoadError)
I created a services/ folder to put the Google Calendar related code inside, I wasn't sure if I should put is in a controller though. How should I organize this?
Important consideration:
I'm not using any other method of Authentication/Authorization such as Devise or others and I'm not planning to do so right now. I just want to get Google's Authorization token and create a Calendar event.
Github Project Link
I was able to figure this out and will post the answer to each of the questions below:
One of the possible locations for the client_secret.json file is config/client_secret.json.
When shipping to Production in Heroku, use ENV Vars.
Require is not the appropriate way to import the credentials in the json file.
Use Google::APIClient::ClientSecrets.load( File.join( Rails.root, 'config', 'client_secret.json' ) ) (assuming the file is indeed in config/.
There are several different alternatives as on how to organize code. I ended up creating a services folder and a google_calendar.rb class holding the authorization logic.
Here's the code:
app/services/google_calendar.rb
require 'google/api_client/client_secrets'
require 'google/apis/calendar_v3'
class GoogleCalendar
# Attributes Accessors (attr_writer + attr_reader)
attr_accessor :auth_client, :auth_uri, :code
def initialize
# ENV: Development
# Google's API Credentials are in ~/config/client_secret.json
client_secrets = Google::APIClient::ClientSecrets.load( File.join( Rails.root, 'config', 'client_secret.json' ) )
#auth_client = client_secrets.to_authorization
# Specify privileges and callback URL
#auth_client.update!(
:scope => 'https://www.googleapis.com/auth/calendar',
:redirect_uri => 'http://localhost:3000/oauth2callback'
)
# Build up the Redirecting URL
#auth_uri = #auth_client.authorization_uri.to_s
end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Starting action in config/routes.rb
def welcome
# Redirect to Google Authorization Page
redirect_to GoogleCalendar.new.auth_uri
end
def token
# Get a auth_client object from Google API
#google_api = GoogleCalendar.new
#google_api.auth_client.code = params[:code] if params[:code]
response = #google_api.auth_client.fetch_access_token!
session[:access_token] = response['access_token']
# Whichever Controller/Action needed to handle what comes next
redirect_to new_event_path()
end
end
Related
Hi I am newbie in rails and wanted to know any way to list folders and files for a url say public . So example user types example.com/public he gets all folders and files listed as we do in php if we start a wamp server . I need this so I can create files and have their url shared to public i.e. simply send url link online like example.com/public/test.pdf . Currently I am getting page with no routes defined .enter image description here
Thanks.
Create a controller to serve the file using a route parameter, ex.
get '/public/:filename', to: 'file#serve'
Then use the send_file method in your controller.
class FileController < ApplicationController
def serve
# For authorization:
#authorize!
# For authentication: (a way or another)
#redirect_to :access_denied unless user_signed_in?
send_file "/www/yourapp/var/files/#{params[:filename]}"
end
end
This way the file can be anywhere within your app, or even on a Cloud storage. It also gives you the ability to use authentication or authorization to check if the user has access.
Please note that this code is very simple, but there are much more options like Fog gem for Cloud storage and everything else.
See https://apidock.com/rails/ActionController/Streaming/send_file for more information.
I've been using the gem LinkedIn OAuth 2.0. Right now I can get it to generate the linkedin signin page. However, the next thing that is supposed to happen is it sends to my callback link a code which I use to generate an access token. The problem is that the variable 'oauth' is generated in the authenticate action but then needs to be used again in the callback action. I've tried generating the oauth variable again using the exact same parameters, but when I do that I get an SSL certificate error. It seems like the exact same oauth instance needs to be used in both cases. Let me know if you have any thoughts. My code is below:
def authenticate
require "linkedin-oauth2"
LinkedIn.configure do |config|
config.client_id = "Mycode"
config.client_secret = "Mysecret"
# This must exactly match the redirect URI you set on your application's
# settings page. If your redirect_uri is dynamic, pass it into
# `auth_code_url` instead.
config.redirect_uri = "http://localhost:3000/auth/linkedin/callback"
end
oauth = LinkedIn::OAuth2.new()
url = oauth.auth_code_url
redirect_to url
end
def callback
require "linkedin-oauth2"
code = params[:code]
access_token = oauth.get_access_token(code)
api = LinkedIn::API.new(access_token)
my_job_titles = api.profile(fields: ["id", {"positions" => ["title"]}])
puts my_job_titles
redirect_to("/")
end
end
Getting an SSL certificate error doesn't mean that the instantiation is wrong. I don't know that gem, but I can't see why would that be a problem.
The require and the configuration block should not be inside the method (maybe you forgot the configuration from the second method?); the best place for those is in config/initializers/linkedin_oauth2.rb.
If you don't want to load it at startup, then you can put those in a private method oauth with memoization:
def oauth
#oauth ||=
begin
require "linkedin-oauth2"
LinkedIn.configure do |config|
...
end
LinkedIn::OAuth2.new()
end
end
If the SSL error still occurs, you should investigate that. You can try creating a simple Ruby script with some example from the gem's readme, just to test the connection to LinkedIn.
Looks like the gem is using the faraday gem for HTTP, you can also try using that directly to make a simple call to LinkedIn.
Using Rails, Devise and devise_cas_authenticatable, I want to use a different CAS server depending on some internal configuration.
Since cas_base_url is set up in an initializer, the value is re-written like this in a before filter:
Devise.setup do |config|
config.cas_base_url="my_custom_variable_cas_server"
end
As soon as the before_filter is called, and then :authenticate_user!, in the first run the custom cas server is set, but after that it's cached or something and never modified.
In the environments/development.rb, config.cache_classes = false.
Is there any way to extend Devise or override the function that specifies which is the login url for devise_cas_authenticatable?
The solution was to redefine the 'new' action in the cas gem in a lib file:
Create a new file in /lib called
devise_cas_authenticatable_extender.rb, or any other name you like.
touch lib/devise_cas_authenticatable_extender.rb
Copy and paste the following
text inside
# This library is necessary for CAS authentication to work dynamically
# Note: if called from application controller it raises a circular dependency ...
# ... error when using thin (not with rails s)
# It has to be required in every controller that uses CAS authentication
Devise::CasSessionsController.class_eval do
# We redefine the behaviour of the new action to be dynamic
def new
domain=request.protocol + ((request.port.blank? || request.port==80) ? request.host : "#{request.host}:#{request.port}") # Production and development
servicio=domain+"/users/service"
servicio=servicio.gsub(':', '%3A').gsub('/', '%2F') # This can probably be done with URI
url="https://your_cas_server_domain/#{session[:dynamic_url_path]}/login?service=#{servicio}"
redirect_to url
end
end
Insert the following in the controller(s) that use
authentication (note: if you use application_controller.rb, you may
get a circular_dependency error, I didn't get into solving that
since I don't use application_controller.rb for session stuff).
# This library is necessary for CAS authentication to work dynamically
# Note: if called from application controller it raises a circular dependency ...
# ... error when using thin (not with rails s)
# It has to be required in every controller that uses CAS authentication
require 'devise_cas_authenticatable_extender'
Somewhere in your code, set the session[:dynamic_url_path] variable to something that makes sense in your project, like:
session[:dynamic_url_path] = request.path_parameters[:your_routes_path_variable_here]
That's it :)
I've used this solution and I was getting the Circular dependency error with the application running in production environment (because of the eager_load configuration).
Finally I got it working inserting the require 'devise_cas_authenticatable_extender' in the config/environments.rb file.
I have a Rails application hosted on Heroku. The app generates and stores PDF files on Amazon S3. Users can download these files for viewing in their browser or to save on their computer.
The problem I am having is that although downloading of these files is possible via the S3 URL (like "https://s3.amazonaws.com/my-bucket/F4D8CESSDF.pdf"), it is obviously NOT a good way to do it. It is not desirable to expose to the user so much information about the backend, not to mention the security issues that rise.
Is it possible to have my app somehow retrieve the file data from S3 in a controller, then create a download stream for the user, so that the Amazon URL is not exposed?
You can create your s3 objects as private and generate temporary public urls for them with url_for method (aws-s3 gem). This way you don't stream files through your app servers, which is more scalable. It also allows putting session based authorization (e.g. devise in your app), tracking of download events, etc.
In order to do this, change direct links to s3 hosted files into links to controller/action which creates temporary url and redirects to it. Like this:
class HostedFilesController < ApplicationController
def show
s3_name = params[:id] # sanitize name here, restrict access to only some paths, etc
AWS::S3::Base.establish_connection!( ... )
url = AWS::S3::S3Object.url_for(s3_name, YOUR_BUCKET, :expires_in => 2.minutes)
redirect_to url
end
end
Hiding of amazon domain in download urls is usually done with DNS aliasing. You need to create CNAME record aliasing your subdomain, e.g. downloads.mydomain, to s3.amazonaws.com. Then you can specify :server option in AWS::S3::Base.establish_connection!(:server => "downloads.mydomain", ...) and S3 gem will use it for generating links.
Yes, this is possible - just fetch the remote file with Rails and either store it temporarily on your server or send it directly from the buffer. The problem with this is of course the fact that you need to fetch the file first before you can serve it to the user. See this thread for a discussion, their solution is something like this:
#environment.rb
require 'open-uri'
#controller
def index
data = open(params[:file])
send_data data, :filename => params[:name], ...
end
This issue is also somewhat related.
First you need create a CNAME in your domain, like explain here.
Second you need create a bucket with the same name that you put in CNAME.
And to finish you need add this configurations in your config/initializers/carrierwave.rb:
CarrierWave.configure do |config|
...
config.asset_host = 'http://bucket_name.your_domain.com'
config.fog_directory = 'bucket_name.your_domain.com'
...
end
I have been struggling with a problem for the past days in a Ruby on Rails App I'm currently working on. I have different countries and for each country we use different Amazon S3 buckets. Amazon S3 key credentials are stored as constants in config/environments/environment_name.rb(ex:demo.rb) There is no way for me to determine which country we are operating from the config file. I can determine which country we are operating from the controllers,models,views,etc but not from the config file. Is there a Ruby meta programming or some other kind of magic that I'm not aware of so that I want to say if we are working on UK as a country in the app, use UK's bucket credentials or Germany as a country, use Germany's bucket credentials? I can't think of a way to pass parameters to environment files from the app itself. Thank you very much in advance for all your helps.
Rather than actually pass the configuration details to whichever S3 client you're using at launch, you should probably select the relevant credentials for each request. Your config file can define them all in a hash like so:
# config/s3.rb
S3_BUCKETS => {
:us => 'our-files-us',
:gb => 'our-files-gb',
:tz => 'special-case'
}
Then you can select the credentials on request like so (in maybe your AppController):
bucket_name = S3_BUCKETS[I18n.locale]
# pass this info to your S3 client
Make sense?
Write a little middleware if you want to keep the knowledge of the per-country configuration out of the main application.
A middleware is extremely simple. A do-nothing middleware looks like this:
class DoesNothing
def initialize(app, *args)
#app = app
end
def call(env)
#app.call(env)
end
end
Rack powers applications through chaining a series of middlewares together... each one is given a reference to #app, which is the next link in the chain, and it must invoke #call on that application. The one at the end of the chain runs the app.
So in your case, you can do some additional configuration in here.
class PerCountryConfiguration
def initialize(app)
#app = app
end
def call(env)
case env["COUNTRY"]
when "AU"
Rails.application.config.s3_buckets = { ... }
when "US"
Rails.application.config.s3_buckets = { ... }
... etc
end
#app.call(env)
end
end
There are several ways to use the middleware, but since it depends on access to the Rails environment, you'll want to do it from inside Rails. Put it in your application.rb:
config.middleware.use PerCountryConfiguration
If you want to pass additional arguments to the constructor of your middleware, just list them after the class name:
config.middleware.use PerCountryConfiguration, :some_argument
You can also mount the middleware from inside of ApplicationController, which means all of the initializers and everything will have already been executed, so it may be too far along the chain.