stripe webhooks for rails 4 - ruby-on-rails

I have been trying to setup my first webhook with stripe. I found an article that looks like the right way to do it but 2 years old. I am thinking it is outdated.
Here is my controller so far.
class StripewebhooksController < ApplicationController
# Set your secret key: remember to change this to your live secret key in production
# See your keys here https://manage.stripe.com/account
Stripe.api_key = "mytestapikey"
require 'json'
post '/stripewebhooks' do
data = JSON.parse request.body.read, :symbolize_names => true
p data
puts "Received event with ID: #{data[:id]} Type: #{data[:type]}"
# Retrieving the event from the Stripe API guarantees its authenticity
event = Stripe::Event.retrieve(data[:id])
# This will send receipts on succesful invoices
# You could also send emails on all charge.succeeded events
if event.type == 'invoice.payment_succeeded'
email_invoice_receipt(event.data.object)
end
end
end
Will this work correctly and is this the right way to do it? Here is the stripe documentation.

I'm using Stripe Webhooks in production and this doesn't look quite right. You should first define your webhook URL in your routes like this:
# config/routes.rb
MyApp::Application.routes.draw do
post 'webhook/receive'
end
In this example your webhook url will be at http://yourapp.com/webhook/receive (that's what you give to Stripe). Then you need the appropriate controller and action:
class WebhookController < ApplicationController
# You need this line or you'll get CSRF/token errors from Rails (because this is a post)
skip_before_filter :verify_authenticity_token
def receive
# I like to save all my webhook events (just in case)
# and parse them in the background
# If you want to do that, do this
event = Event.new({raw_body: request.body.read})
event.save
# OR If you'd rather just parse and act
# Do something like this
raw_body = request.body.read
json = JSON.parse raw_body
event_type = json['type'] # You most likely need the event type
customer_id = json['data']['object']['customer'] # Customer ID is the other main bit of info you need
# Do the rest of your business here
# Stripe just needs a 200/ok in return
render nothing: true
end
end
Another thing to note: every webhook you receive has an ID. It's good practice to save and check against this to make sure you're not acting on the same event more than once.

Related

HTTParty: Post action is resulting in error Net::HTTPServerException (403 "Forbidden")

I am trying to implement post action using httparty gem and this is what I have. I am running everything in docker and I have code below that will run as active job. I is in one service and I am trying to make post to api in other service. I am able to do get but not having any luck with post. I looked and searched a lot online but I am not sure what is it I am doing wrong. I always get error 403 at self.class.post line. I also tried to do a postman call to api and I am able to hit the api but with the code below its not even reaching to the other service.
Any help is appreciated. Thanks.
require 'uri'
class CustomerProductAPI
include HTTParty
format :json
def initialize(customer_product_id)
#customer_product = CustomerProduct.find(customer_product_id)
#customer = Customer.find(#customer_product.student_id)
#product = Product.find(#customer_product.product_id)
self.class.base_uri environment_based_uri + '/customer_product_api'
end
def create_customer_product
uri = URI(self.class.base_uri + "/customer/#{customer.id}")
self.class.post(uri, body: body_hash).response.value
end
private
attr_reader :customer_product, :customer, :product
def body_hash
{
token: ENV['CUSTOMER_PRODUCT_API_TOKEN'],
customer: customer.name,
product: product.name,
}
end
def environment_based_uri
ENV['CUSTOMER_PRODUCT_URL']
end
end
While we can't actually be sure of exactly what the server thats accepting the response expects you're definately doing quite a few non-idiomatic things here which will aggrevate trouble shooting.
base_uri should just be set in the class body. Not in initialize for each instance. You also do not need to construct a URI with HTTParty. Just pass a path and it will construct the request uri relative to the base_uri.
When getting configuration from ENV use ENV.fetch instead of the bracket accessors as it will raise a KeyError instead of just letting a nil sneak through.
Your HTTP client class should not be concerned with querying the database and handling the potential errors that can occur if the records cannot be found. That should be the responsibility of the controller/job/service object that calls the client. Since you're only actually using three simple attributes it doesn't actually need records at all as input and its actually better that it doesn't have to know about your models and their assocations (or lack thereof in this case).
class CustomerProductAPI
# lets you stub/inspect the constant
CUSTOMER_PRODUCT_URL = ENV.fetch('CUSTOMER_PRODUCT_URL') + '/customer_product_api'
include HTTParty
format :json
base_uri CUSTOMER_PRODUCT_URL
def initialize(id:, product_name:, customer_name:)
#id = id
#product_name = product_name
#customer_name = customer_name
end
def create_customer_product
self.class.post("/customer/#{#id}", body: {
token: ENV.fetch('CUSTOMER_PRODUCT_API_TOKEN'),
customer: #customer_name,
product: #product_name
})
# don't return .response.value as it will make error handling impossible.
# either handle unsuccessful responses here or return the whole response
# for the consumer to handle it.
end
end

How to pass arguments to initialize method from a class?

I am using Push8 gem for Apple push notification which accepts .P8 certificates.The problem is that I have two bundle_id for two separate apps and need to send Push Notifications to both of them. The Push8 gem accepts bundle_id ENV['APN_BUNDLE_ID'] params automatically from the application.yml file. However, I want it to use ENV['APN_VENDOR_BUNDLE_ID'] as well for a different APP to send Push Notification.
my code to send Push notification is here
def self.send_notification_ios(device_id, notification_id)
send = Notification.where(id: notification_id).first
if Rails.env == 'development'
apn = P8push::Client.development
else
apn = P8push::Client.production
end
token = device_id
notification = P8push::Notification.new(device: token)
notification.alert = send.template.message % { txnid: send.order.txnid }
notification.sound = 'sosumi.aiff'
apn.push(notification)
end
Here If the send.end_user_type is "User" I want to use the Bundle id APN_BUNDLE_ID as the topic, for rest as want to use APN_VENDOR_BUNDLE_ID. But I dont know how to pass APN_VENDOR_BUNDLE_ID as a param to initialize method in the client.rb file of the gem. Hence it always accepts APN_BUNDLE_ID as the topic and hence throws the error topic disallowed.
Here is the client.rb file for the gem:
https://github.com/andrewarrow/p8push/blob/master/lib/p8push/client.rb
The link for the gem is https://github.com/andrewarrow/p8push
If the initialize method makes no accommodations for customizing that attribute you've got two choices: Monkey-patch it to make it do what you want, which is messy, or subclass it and use that instead.
The sub-class solution looks like this:
class UserAwareClient < P8Push::Client
def self.development(user_type)
client = self.new(user_type)
client.jwt_uri = APPLE_DEVELOPMENT_JWT_URI
client
end
def self.production(user_type)
client = self.new(user_type)
client.jwt_uri = APPLE_PRODUCTION_JWT_URI
client
end
def initialize(user_type)
# Initialize as the parent class would
super
# Then detect the user_type argument and decide how to configure it
#private_key =
case (user_type)
when 'User'
File.read(ENV['APN_PRIVATE_KEY'])
else
File.read(ENV['APN_VENDOR_BUNDLE_ID'])
end
end
end
end
Then you create that:
apn = UserAwareClient.development(user_type)
This gem could be made a lot more flexible with a few pull-requests to make your life easier, so consider that, too.

REST Client callback: How to tell when items are done loading?

I'm trying to load some items from this external API using REST Client and DelayedJob.
But, unlike Ajax check for database change using route, these items don't get saved to the database but rather go straight to the view -- so how would I know when they're done loading so I can load them into the client using Ajax?
Live app which you can run on the fly: http://runnable.com/VW9rQx-KiIFfmpII/ajax-affiliates
app/controllers/main_controller.rb
class MainController < ApplicationController
def index
# Delay fetching
#products = Affiliate.delay.fetch
end
# Pseudo-code from SO `ajax-check-for-database-change-using-route`
#
# def check_items_loaded
# #items_status = Affiliate.find(params[:id]).items_loaded
# respond_to do |wants|
# wants.js
# end
# end
end
config/routes.rb
# Pseudo-code from SO `ajax-check-for-database-change-using-route`
# get '/check_items_loaded/:id', to: 'photos#check_items_loaded', as: :check_items_loaded
app/models/affiliate.rb
require "rest_client"
module Affiliate
def self.fetch
response = RestClient::Request.execute(
:method => :get,
:url => "http://api.shopstyle.com/api/v2/products?pid=uid7849-6112293-28&fts=women&offset=0&limit=10"
)
#products = JSON.parse(response)["products"].map do |product|
product = OpenStruct.new(product)
product
end
end
# More pseudo-code
# Add `items_loaded` boolean to database?
# private
# def set_items_loaded
# self.update_attributes(items_loaded: ???)
# end
end
You could look into a pub-sub architecture using something like PubNub[1] or Pusher[2]
Your front-end would subscribe to those endpoints and then you can have your backend publish events to those endpoints (which are then read from the consumers (your frontend))
[1] PubNub - http://www.pubnub.com/
[2] Pusher - https://pusher.com/
You might be able to hack together a solution using ActionController::Streaming. Do take into account potential session bottlenecks.
An alternate solution might be using Websockets & Redis.

how to parse json url response in rails

I've a simple Rails app that makes a request to an external website and returns a json url response with a callback to my site to notify my Rails app about the response. The json url sample is https://voguepay.com/?v_transaction_id=demo-1345109950&type=json and the response body is below:
{"merchant_id":"demo","transaction_id":"demo-1345109950","email":"testuser#buyerdomain.com","total":"10","total_paid_by_buyer":"10.00","total_credited_to_merchant":"9.90","extra_charges_by_merchant":"0.00","merchant_ref":"","memo":"Donation of N10 to Test User","status":"Approved","date":"2012-01-01 11:39:11","referrer":"http:\/\/sellerdomain.com\/buy_now.html","method":"VoguePay","fund_maturity":"2012-01-03"}
I'd like to convert this response into a Rails method than can simply give me the attribute I need to make my query. For example, from the response body I need to make an action like:
def notify
response = Json.parse('https://voguepay.com/?v_transaction_id=demo-1345109950&type=json').body
response.status
response.date
response.merchant_id
response.total
end
The code above is just a sample to explain what I want to achieve. Any help will be great.
I've tried both typhoeus and yajl-ruby gems but when the request comes in, all my authentication methods are removed and I keep getting the error message cannot authenticate csrf meta token. Even if I skip it the current user will be signed out automatically ( with devise authentication). The code sample used is below:
class NotificationsController < ApplicationController
skip_before_filter :verify_authenticity_token
def notify
#transaction_id = params[:transaction_id]
do_notify
end
private
def do_notify
hydra = Typhoeus::Hydra.new
request = Typhoeus::Request.new("https://voguepay.com/?v_transaction_id=#{#transaction_id}&type=json")
request.on_complete do |response|
logger.info("#{response.request.url} in #{response.time} seconds") #remove in production to avoid huge logs
transaction = Yajl::Parser.parse(response.body) #or Yajl::Parser.parse(response.body)
#Now we have the following keys in our transaction hash you can do whatever
transaction[:merchant_id]
transaction[:transaction_id]
transaction[:email]
transaction[:total]
transaction[:merchant_ref]
transaction[:memo]
transaction[:status]
transaction[:date]
transaction[:referrer]
transaction[:method]
#plan = Plan.find_by_id(transaction[:merchant_ref])
if(transaction[:total] == 0)
logger.error "Invalid total for transaction:#{#transaction_id}"
#do not subscribe the user or generate invoice, notify user of error n cancel order
elsif(transaction[:status] != 'Approved')
logger.error "Failed transaction for transaction:#{#transaction_id}"
#do not subscribe the user or generate invoice, notify user of error n cancel order
elsif(transaction[:total] >= #plan.naira_price.to_s)
current_user.award_user_credits(#plan.hjc.to_i)
end
end
hydra.queue(request)
hydra.run
end
end
Anymore, I don't want to use a gem I want to do it manually to see if the csrf meta token will not be affected. So any idea on how I can achieve this will be great. I am using rails 3.2.9.
Thank you!
You can do:
def notify
response = JSON('https://voguepay.com/?v_transaction_id=demo-1345109950&type=json').body
response["status"]
response["date"]
response["merchant_id"]
response["total"]
end

Zendesk Single Sign-on gem for rails 3

Does anyone know of a maintained gem that handles user authentication for the Zendesk API through an existing Rails 3 application?
I asked Zendesk IT and got sent to https://github.com/tobias/zendesk_remote_auth, but it does not look rails 3 compatible and has not been updated since 2009.
I think the article in our docs gives the impression that Zendesk SSO is difficult when in fact it is pretty easy (http://www.zendesk.com/api/remote-authentication).
# reference http://www.zendesk.com/api/remote-authentication
# you need to be a Zendesk account admin to enable remote auth (if you have not already)
# go to Settings > Security, click "Enabled" next to Single Sign-On
# three important things to pay attention to:
# Remote Login URL, Remote Logout URL, and shared secret token
# for testing on a Rails 3 application running on localhost, fill in the Remote Login URL to map
# to http://localhost:3000/zendesk/login (we will need to make sure routes for that exist)
# fill in Remote Logout URL to http://localhost:3000/zendesk/logout
# copy the secret token, you'll need it later
# first, let's create those routes in config/routes.rb
namespace :zendesk do
match "/login" => "zendesk#login" # will match /zendesk/login
match "/logout" => "zendesk#logout" # will match /zendesk/logout
end
# Above I've mapped those requests to a controller named "zendesk" but it can be named anything
# next we want to add our secret token to the application, I added this in an initializer
# config/initializers/zendesk_auth.rb
ZENDESK_REMOTE_AUTH_TOKEN = "< your token >"
ZENDESK_REMOTE_AUTH_URL = "http://yourcompany.zendesk.com/access/remote/"
# Assuming we have a controller called zendesk, in zendesk_controller.rb
require "digest/md5"
class ZendeskController < ApplicationController
def index
#zendesk_remote_auth_url = ZENDESK_REMOTE_AUTH_URL
end
def login
timestamp = params[:timestamp] || Time.now.utc.to_i
# hard coded for example purposes
# really you would want to do something like current_user.name and current_user.email
# and you'd probably want this in a helper to hide all this implementation from the controller
string = "First Last" + "first.last#gmail.com" + ZENDESK_REMOTE_AUTH_TOKEN + timestamp.to_s
hash = Digest::MD5.hexdigest(string)
#zendesk_remote_auth_url = "http://yourcompany.zendesk.com/access/remote/?name=First%20Last&email=first.last#gmail.com&timestamp=#{timestamp}&hash=#{hash}"
redirect_to #zendesk_remote_auth_url
end
def logout
flash[:notice] = params[:message]
end
end
# Note that the above index action defines an instance variable #zendesk_remote_auth_url
# in my example I simple put a link on the corresponding view that hits ZENDESK_REMOTE_AUTH_URL, doing so
# will cause Zendesk to hit your applications Remote Login URL (you defined in your Zendesk SSO settings) and pass a timestamp back in the URL parameters
# BUT, it is entirely possible to avoid this extra step if you just want to go to /zendesk/login in your app
# notice I am either using a params[:timestamp] if one exists or creating a new timestamp with Time.now
This example is quite simplistic but I just want to illustrate the basic mechanics of Zendesk SSO. Note that I'm not touching the more complicated issue of creating new users or editing existing ones, just logging in users who have an existing Zendesk account.
There is an updated example code from zendesk
# Using JWT from Ruby is straight forward. The below example expects you to have `jwt`
# in your Gemfile, you can read more about that gem at https://github.com/progrium/ruby-jwt.
# Assuming that you've set your shared secret and Zendesk subdomain in the environment, you
# can use Zendesk SSO from your controller like this example.
class ZendeskSessionController < ApplicationController
# Configuration
ZENDESK_SHARED_SECRET = ENV["ZENDESK_SHARED_SECRET"]
ZENDESK_SUBDOMAIN = ENV["ZENDESK_SUBDOMAIN"]
def create
if user = User.authenticate(params[:login], params[:password])
# If the submitted credentials pass, then log user into Zendesk
sign_into_zendesk(user)
else
render :new, :notice => "Invalid credentials"
end
end
private
def sign_into_zendesk(user)
# This is the meat of the business, set up the parameters you wish
# to forward to Zendesk. All parameters are documented in this page.
iat = Time.now.to_i
jti = "#{iat}/#{rand(36**64).to_s(36)}"
payload = JWT.encode({
:iat => iat, # Seconds since epoch, determine when this token is stale
:jti => jti, # Unique token id, helps prevent replay attacks
:name => user.name,
:email => user.email,
}, ZENDESK_SHARED_SECRET)
redirect_to zendesk_sso_url(payload)
end
def zendesk_sso_url(payload)
"https://#{ZENDESK_SUBDOMAIN}.zendesk.com/access/jwt?jwt=#{payload}"
end
end

Resources