Ruby on Rails use of methods within methods - ruby-on-rails

So I am writing a simple controller that will receive parameters from a Postrequest to my API. And I want to keep things cleaner and nice, so I wrote something like this:
def create
update_contact
end
def update_contact
create_token
url = URI("https://acme.api-us1.com/api/3/contacts/#{active_campaign_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['api-token'] = API_KEY
data = { contact: { fieldValues: [{ field: '1', value: contact[:email_token] }] } }
request.body = JSON.dump(data)
response = http.request(request)
end
def create_token
active_campaign_id = params[:contact][:id].to_i
generate_token = SecureRandom.urlsafe_base64(12)
contact = Contact.find_or_initialize_by(active_campaign_id: active_campaign_id, email_token: generate_token)
contact.save!
end
But whenever I run this turns into:
*** NameError Exception: undefined local variable or method `active_campaign_id'
and same goes for email_token
*** NameError Exception: undefined local variable or method `email_token'
Now whenever I do this:
def create
update_contact
end
def update_contact
active_campaign_id = params[:contact][:id].to_i
generate_token = SecureRandom.urlsafe_base64(12)
contact = Contact.find_or_initialize_by(active_campaign_id: active_campaign_id, email_token: generate_token)
contact.save!
url = URI("https://acme.api-us1.com/api/3/contacts/#{active_campaign_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['api-token'] = API_KEY
data = { contact: { fieldValues: [{ field: '1', value: contact[:email_token] }] } }
request.body = JSON.dump(data)
response = http.request(request)
end
It works! Why is that? How can I structure my code or methods as clean as possible?
And what resources could make me understand more accessing methods in rails?
Thanks for the help!

If you want to keep methods you have and make it works you can achieve this by doing next refactoring:
def create
update_contact
end
def update_contact
contact = create_contact
url = URI("https://acme.api-us1.com/api/3/contacts/#{contact.active_campaign_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['api-token'] = API_KEY
data = { contact: { fieldValues: [{ field: '1', value: contact.email_token }] } }
request.body = JSON.dump(data)
response = http.request(request)
end
def create_contact
Contact.create_with(
email_token: SecureRandom.urlsafe_base64(12)
).find_or_create_by!(
active_campaign_id: params.dig(:contact, :id)&.to_i
)
end
And probably you need to use create_with method because every time when you will try to find Contact by fields pair email_token and active_campaign_id SecureRandom.urlsafe_base64(12) will generate a new email token and you always will have new object created instead of getting it from database.

It looks like you are trying to access the 'active_campaign_id' and 'email_token' variables in the 'update_contact' method, but those variables are only defined in the 'create_token' method. Try moving the 'update_contact' method inside the 'create_token' method so that it has access to those variables.
More info you could find here:
https://guides.rubyonrails.org/v6.1/action_view_overview.html#using-action-view-with-rails
https://guides.rubyonrails.org/v6.1/engines.html#using-a-controller-provided-by-the-application
https://guides.rubyonrails.org/v6.1/security.html#user-management
https://guides.rubyonrails.org/v6.1/2_3_release_notes.html#action-controller
https://guides.rubyonrails.org/v6.1/active_record_multiple_databases.html#automatic-swapping-for-horizontal-sharding

Related

Refactoring an API request on Rails

I have a rails worker using redis/sidekiq where I send some data to an API (Active Campaign), so I normally use all the http configurations to send data. I want to have it nice and clean, so it's part of a refactor thing. My worker currently looks like this:
class UpdateLeadIdWorker
include Sidekiq::Worker
BASE_URL = Rails.application.credentials.dig(:active_campaign, :url)
private_constant :BASE_URL
API_KEY = Rails.application.credentials.dig(:active_campaign, :key)
private_constant :API_KEY
def perform(ac_id, current_user_id)
lead = Lead.where(user_id: current_user_id).last
url = URI("#{BASE_URL}/api/3/contacts/#{ac_id}") #<--- need this endpoint
https = bindable_lead_client.assign(url)
pr = post_request.assign(url)
case lead.quote_type
when 'renter'
data = { contact: { fieldValues: [{ field: '5', value: lead.lead_id }] } }
when 'home'
data = { contact: { fieldValues: [{ field: '4', value: lead.lead_id }] } }
when 'auto'
data = { contact: { fieldValues: [{ field: '3', value: lead.lead_id }] } }
else
raise 'Invalid quote type'
end
pr.body = JSON.dump(data)
response = JSON.parse(https.request(pr).read_body).symbolize_keys
if response.code == '200'
Rails.logger.info "Successfully updated contact #{ac_id} with lead id #{lead.lead_id}"
else
raise "Error creating contact: #{response.body}"
end
end
def bindable_lead_client
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http
end
def post_request
post_request_ = Net::HTTP::Put.new(url)
post_request_['Accept'] = 'application/json'
post_request_['Content-Type'] = 'application/json'
post_request_['api-token'] = API_KEY
post_request_
end
end
But whenever I run this I get:
2022-07-28T00:52:08.683Z pid=24178 tid=1s1u WARN: NameError: undefined local variable or method `url' for #<UpdateLeadIdWorker:0x00007fc713442be0 #jid="e2b9ddb6d5f4b8aecffa4d8b">
Did you mean? URI
I don't want everything stuck in one method. How could I achieve to make this cleaner?
Thanks.
Pure ruby wise, The reason you get the error is because your method definition bindable_lead_client is missing the url argument. Hence undefined variable.
So def should look something like:
def bindable_lead_client (url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http
end
and call:
bindable_lead_client(url)
As for how to make this code better, falls under question being too subjective under StackOverflow guidelines, which encourage you to ask more specific questions.

FTX.com REST API POST Authentication FAILS with Ruby on Rails and net/https

Hoping for some help as this one has me baffled...
I created a user account and API credentials at FTX.com.
They have an interesting Auth setup which is detailed here: https://docs.ftx.com/?python#authentication
They only provide code examples for python, javascript and c#, but I need to implement the integration on a RoR app.
Here's a link which also provides an example for both GET and POST calls: https://blog.ftx.com/blog/api-authentication/
I'm using:
ruby '3.0.1'
gem 'rails', '~> 6.1.4', '>= 6.1.4.1'
also,
require 'uri'
require 'net/https'
require 'net/http'
require 'json'
I got the authentication working for GET calls as follows:
def get_market
get_market_url = 'https://ftx.com/api/markets/BTC-PERP/orderbook?depth=20'
api_get_call(get_market_url)
end
def api_get_call(url)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}GET/api/markets"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
rsp = http.get(uri, headers)
JSON.parse(rsp.body)
end
This works great and I get the correct response:
=>
{"success"=>true,
"result"=>
{"bids"=>
[[64326.0, 2.0309],
...
[64303.0, 3.1067]],
"asks"=>
[[64327.0, 4.647],
...
[64352.0, 0.01]]}}
However, I can't seem to authenticate correctly for POST calls (even though as far as I can tell I am following the instructions correctly). I use the following:
def create_subaccount
create_subaccount_url = 'https://ftx.com/api/subaccounts'
call_body =
{
"nickname": "sub2",
}.to_json
api_post_call(create_subaccount_url, call_body)
end
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri, headers)
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
Also tried passing headers via request[] directly:
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['FTX-KEY'] = ENV['FTX_API_KEY']
request['FTX-SIGN'] = signature
request['FTX-TS'] = ts.to_s
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
This is the error response:
=> {"success"=>false, "error"=>"Not logged in: Invalid signature"}
My feeling is the issue is somewhere in adding the body to signature_payload before generating the signature via HMAC here..?:
signature_payload = "#{ts}POST/api/subaccounts#{body}"
Thinking this because, if I leave out #{body} here, like so:
signature_payload = "#{ts}POST/api/subaccounts"
the response is:
=> {"success"=>false, "error"=>"Missing parameter nickname"}
I have tried several iterations of setting up the POST call method using various different net/https examples but have had no luck...
I have also contacted FTX support but have had no response.
Would truly appreciate if anyone has some insight on what I am doing wrong here?
try this headers
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
Here's a working example of a class to retrieve FTX subaccounts. Modify for your own purposes. I use HTTParty.
class Balancer
require 'uri'
require "openssl"
include HTTParty
def get_ftx_subaccounts
method = 'GET'
path = '/subaccounts'
url = "#{ENV['FTX_BASE_URL']}#{path}"
return HTTParty.get(url, headers: headers(method, path, ''))
end
def headers(*args)
{
'FTX-KEY' => ENV['FTX_API_KEY'],
'FTX-SIGN' => signature(*args),
'FTX-TS' => ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
end
def signature(*args)
OpenSSL::HMAC.hexdigest(digest, ENV['FTX_API_SECRET'], signature_payload(*args))
end
def signature_payload(method, path, query)
payload = [ts, method.to_s.upcase, "/api", path].compact
if method==:post
payload << query.to_json
elsif method==:get
payload << ("?" + URI.encode_www_form(query))
end unless query.empty?
payload.join.encode("UTF-8")
end
def ts
#ts ||= (Time.now.to_f * 1000).to_i
end
def digest
#digest ||= OpenSSL::Digest.new('sha256')
end
end

Why does HERE oauth2 token request api return 401300 with Rails but works fine with Postman?

When calling the HERE authentication service (https://account.api.here.com/oauth2/token) from one of the controllers of the RoR APP (Rails 5.0.6/ruby 2.6.1) I get a 401: "401300 Signature mismatch. Authorization signature or client credential is wrong"
The Key, secret, Authorization header, content type, request body etc ... are the same as the ones used by Postman.
Postman always returns a 200 OK but the rails app systematically returns "401"
Any suggestions on what the problem is?
def fetch_new_token
# URL
api_url = 'https://account.api.here.com/oauth2/token'
# VERSION
api_version='1.0'
# GRANT TYPE
api_grant_type_for_req_body='grant_type=client_credentials'
#KEY
api_access_key_id = CGI.escape(ENV['my_access_key_id'])
#SECRET
api_access_key_secret = CGI.escape(ENV['my_access_key_secret'])
#NONCE
draft_api_nonce= [('a'..'z'), ('A'..'Z')].map(&:to_a).flatten
api_nonce=(0...20).map { draft_api_nonce[rand(draft_api_nonce.length)] }.join
#TMESTAMP
api_timestamp = (Time.now).strftime('%s')
#NORMALIZED URL
api_url_normalized = CGI.escape(api_url)
#SIGNING METHOD
api_signature_method= CGI.escape('HMAC-SHA256')
#OAUTH PARAMETERS BASE STRING
api_parameters_string=('consumer_key='+api_access_key_id+'&nonce='+api_nonce+'&signature_method='+api_signature_method+'&timestamp='+api_timestamp+'&'+'version=1.0')
#ENCODED BASE STRING
api_normalized_string = 'POST&'+api_url_normalized+'&'+api_grant_type_for_req_body+CGI.escape('&'+api_parameters_string)
#SIGNNG KEY
api_signing_key = api_access_key_secret+'&'
#SIGNATURE
digest = OpenSSL::Digest.new('sha256')
api_signature = OpenSSL::HMAC.hexdigest(digest, api_normalized_string, api_signing_key)
# convert the HASHING result to a URL ENCODED base64 string.
api_signature_encoded = (Base64.strict_encode64(api_signature))
# AUTHORIZATION STRING - ESCAPED
api_authorization_string = ('OAuth consumer_key="'+api_access_key_id+'",signature_method="'+api_signature_method+'",timestamp="'+CGI.escape(api_timestamp)+'",nonce="'+CGI.escape(api_nonce)+'",version="'+CGI.escape(api_version)+'",signature="'+CGI.escape(api_signature_encoded)+'"')
# FARADAY OBJECT
connect_token_request = Faraday.new(url: 'https://account.api.here.com') do |faraday|
faraday.response :logger, nil, bodies: true
faraday.request :json
faraday.headers['Accept'] = 'application/json'
faraday.headers['Content-Type'] = 'application/x-www-form-urlencoded'
faraday.headers['Authorization'] = api_authorization_string
faraday.adapter Faraday.default_adapter
end
# FARADAY POST
response_token_request= connect_token_request.post('/oauth2/token', 'grant_type=client_credentials' )
# CHECK THE RESULT
puts response_token_request.body
#json = JSON.parse(response_token_request.body)
req_status = #json['httpStatus']
puts "The status returned in the body is:::: #{req_status}"
puts "===== ///// ======"
puts "===== ///// ======"
req_error_code = #json['errorCode']
puts "The ERROR CODE returned in the body is:::: #{req_error_code}"
end
I don't know RoR but I had the same problem in Javascript and this script solved my problem:
const axios = require('axios')
const cryptoJS = require('crypto-js');
const btoa = require('btoa');
exports.getToken = (app_key, app_secret) => {
let url = "https://account.api.here.com/oauth2/token";
let key = encodeURI(app_key);
let secret = encodeURI(app_secret);
let nonce = btoa(Math.random().toString(36)).substring(2, 13);
let timestamp = Math.floor(Date.now()/1000);
let normalizedUrl = encodeURIComponent(url);
let signing_method = encodeURI("HMAC-SHA256");
let sig_string = "oauth_consumer_key="
.concat(key)
.concat("&oauth_nonce=")
.concat(nonce)
.concat("&oauth_signature_method=")
.concat(signing_method)
.concat("&oauth_timestamp=")
.concat(timestamp)
.concat("&").concat("oauth_version=1.0");
let normalised_string = "POST&".concat(normalizedUrl).concat("&").concat(encodeURIComponent(sig_string));
let signingKey = secret.concat("&");
let digest = cryptoJS.HmacSHA256(normalised_string, signingKey);
let signature = cryptoJS.enc.Base64.stringify(digest);
let auth = 'OAuth oauth_consumer_key="'
.concat(key)
.concat('",oauth_signature_method="')
.concat(signing_method)
.concat('",oauth_signature="')
.concat(encodeURIComponent(signature))
.concat('",oauth_timestamp="')
.concat(timestamp)
.concat('",oauth_nonce="')
.concat(nonce)
.concat('",oauth_version="1.0"')
return axios({
method: 'post',
url: url,
data: JSON.stringify({grantType: "client_credentials"}),
headers: {
'Content-Type': "application/json",
'Authorization': auth
}
});
}

Stubbing result of Net::HTTP.new(url.host, url.port)

I have a funciton that calls Net::HTTP.new(google_url.host, google_url.port) and I am trying to figure out how to stub the result for testing. Basically I don't want to be hitting the google URL shortener every time I run my test.
def shorten_url(long_url)
google_url = URI.parse("https://www.googleapis.com/urlshortener/v1/url")
data = JSON.generate({"longUrl" => "#{long_url}"})
header = {"Content-Type" => "application/json"}
http = Net::HTTP.new(google_url.host, google_url.port)
http.use_ssl = true
short_url = ""
res = http.request_post(google_url.path, data, header)
jsonResponse = JSON.parse(res.body)
short_url = jsonResponse["id"]
end
Basically I want to be able to set the result of that function.
I've tried things like: Net::HTTP.any_instance.stubs(:HTTP.new).returns("www.test.com") but cannot figure out how to get it to work.
class HTTP < Protocol
....
# Creates a new Net::HTTP object.
# If +proxy_addr+ is given, creates an Net::HTTP object with proxy support.
# This method does not open the TCP connection.
def HTTP.new(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil)
h = Proxy(p_addr, p_port, p_user, p_pass).newobj(address, port)
h.instance_eval {
#newimpl = ::Net::HTTP.version_1_2?
}
h
end
....
end
Check out webmock - this sounds like it will do exactly what you're looking for.

How to get original curl request made by patron gem in rails

I am using patron gem for creating curl request. Here is the code
s = Patron::Session.new
s.connect_timeout = 15
s.timeout = 15
s.base_url = API_URL
s.headers['Date'] = DATE_HEADER
s.headers['Accept'] = 'application/xml'
s.headers['Content-Type'] = 'application/json'
s.headers['Authorization'] = "API" + " " + auth_token
response = s.delete(uri)
response.status
How can I get the original curl request made by this gem?
You can't. Err... you can, by monkey_patching the Patron::Session.request method, and yielding the request just before the handling. But beware that this is not the "libcurl" request, as this only exists in C code, it's a Patron::Request instance.
Also, beware that your monkey-patching may break at any time, as you have to rewrite the whole method!
You add a yield just before handling the request to libcurl, so you have the opportunity to get it with a block.
req = nil
response = s.delete(uri) do |r|
req = r
end
# now req should be your request instance.
Here is a hint for a patch:
class Patron::Session
# You have to patch all the standard methods to accept a block
def get(url, headers = {}, &block)
request(:get, url, headers, &block)
end
# do the same for get_file, head, delete, put, put_file, post, post_file and post_multipart
def request(action, url, headers, options = {}, &block)
# If the Expect header isn't set uploads are really slow
headers['Expect'] ||= ''
req = Request.new
req.action = action
req.headers = self.headers.merge headers
req.timeout = options.fetch :timeout, self.timeout
req.connect_timeout = options.fetch :connect_timeout, self.connect_timeout
req.max_redirects = options.fetch :max_redirects, self.max_redirects
req.username = options.fetch :username, self.username
req.password = options.fetch :password, self.password
req.proxy = options.fetch :proxy, self.proxy
req.proxy_type = options.fetch :proxy_type, self.proxy_type
req.auth_type = options.fetch :auth_type, self.auth_type
req.insecure = options.fetch :insecure, self.insecure
req.ignore_content_length = options.fetch :ignore_content_length, self.ignore_content_length
req.buffer_size = options.fetch :buffer_size, self.buffer_size
req.multipart = options[:multipart]
req.upload_data = options[:data]
req.file_name = options[:file]
base_url = self.base_url.to_s
url = url.to_s
raise ArgumentError, "Empty URL" if base_url.empty? && url.empty?
uri = URI.join(base_url, url)
query = uri.query.to_s.split('&')
query += options[:query].is_a?(Hash) ? Util.build_query_pairs_from_hash(options[:query]) : options[:query].to_s.split('&')
uri.query = query.join('&')
uri.query = nil if uri.query.empty?
url = uri.to_s
req.url = url
yield req if block_given? # added line
handle_request(req)
end
end

Resources