require 'sinatra/base'
require 'sinatra/activerecord'
require 'attr_encrypted'
require 'active_support/all'
require 'shopify_api'
require 'omniauth-shopify-oauth2'
module Sinatra
module Shopify
module Methods
# designed to be overriden
def after_shopify_auth
end
def logout
session.delete(:shopify)
session.clear
end
# for the esdk initializer
def shop_origin
"https://#{session[:shopify][:shop]}"
end
def shopify_session(&blk)
return_to = request.path
return_params = request.params
if no_session?
authenticate(return_to, return_params)
elsif different_shop?
logout
authenticate(return_to, return_params)
else
shop_name = session[:shopify][:shop]
token = session[:shopify][:token]
activate_shopify_api(shop_name, token)
yield shop_name
end
rescue ActiveResource::UnauthorizedAccess
clear_session shop_name
redirect request.path
end
def shopify_webhook(&blk)
return unless verify_shopify_webhook
shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
webhook_body = ActiveSupport::JSON.decode(request.body.read.to_s)
yield shop_name, webhook_body
status 200
end
private
def request_protocol
request.secure? ? 'https' : 'http'
end
def base_url
"#{request_protocol}://#{request.env['HTTP_HOST']}"
end
def no_session?
!session.key?(:shopify)
end
def different_shop?
params[:shop].present? && session[:shopify][:shop] != sanitize_shop_param(params)
end
def authenticate(return_to = '/', return_params = nil)
if shop_name = sanitized_shop_name
session[:return_params] = return_params if return_params
redirect_url = "/auth/shopify?shop=#{shop_name}&return_to=#{base_url}#{return_to}"
redirect_javascript redirect_url
else
redirect '/install'
end
end
def activate_shopify_api(shop_name, token)
api_session = ShopifyAPI::Session.new(domain: shop_name, token: token, api_version: settings.api_version)
ShopifyAPI::Base.activate_session(api_session)
end
def clear_session(shop_name)
logout
shop = Shop.find_by(name: shop_name)
shop.token = nil
shop.save
end
def redirect_javascript(url)
erb %(
Redirecting…
), layout: false
end
def sanitized_shop_name
@sanitized_shop_name ||= sanitize_shop_param(params)
end
def sanitize_shop_param(params)
return unless params[:shop].present?
name = params[:shop].to_s.strip
name += '.myshopify.com' if !name.include?('myshopify.com') && !name.include?('.')
name.gsub!('https://', '')
name.gsub!('http://', '')
u = URI("http://#{name}")
u.host.ends_with?('.myshopify.com') ? u.host : nil
end
def verify_shopify_webhook
data = request.body.read.to_s
digest = OpenSSL::Digest.new('sha256')
calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, settings.shared_secret, data)).strip
request.body.rewind
if calculated_hmac == request.env['HTTP_X_SHOPIFY_HMAC_SHA256']
true
else
puts 'Shopify Webhook verifictation failed!'
false
end
end
end
def self.registered(app)
app.helpers Shopify::Methods
app.register Sinatra::ActiveRecordExtension
app.enable :inline_templates
app.set :database_file, File.expand_path('config/database.yml')
app.set :views, File.expand_path('views')
app.set :public_folder, File.expand_path('public')
app.set :erb, layout: :'layouts/application'
app.set :protection, except: :frame_options
app.set :api_version, '2019-07'
app.set :scope, 'read_products, read_orders'
app.set :api_key, ENV['SHOPIFY_API_KEY']
app.set :shared_secret, ENV['SHOPIFY_SHARED_SECRET']
app.set :secret, ENV['SECRET']
app.use Rack::MethodOverride
app.use Rack::Session::Cookie, key: 'rack.session',
path: '/',
secure: true,
same_site: 'None',
secret: app.settings.secret,
expire_after: 60 * 30 # half an hour in seconds
app.use OmniAuth::Builder do
provider :shopify,
app.settings.api_key,
app.settings.shared_secret,
scope: app.settings.scope,
setup: lambda { |env|
params = Rack::Utils.parse_query(env['QUERY_STRING'])
site_url = "https://#{params['shop']}"
env['omniauth.strategy'].options[:client_options][:site] = site_url
}
end
ShopifyAPI::Session.setup(
api_key: app.settings.api_key,
secret: app.settings.shared_secret
)
app.get '/install' do
if params[:shop].present?
authenticate
else
erb :install, layout: false
end
end
app.post '/login' do
authenticate
end
app.get '/logout' do
logout
redirect '/install'
end
app.get '/auth/shopify/callback' do
shop_name = params['shop']
token = request.env['omniauth.auth']['credentials']['token']
shop = Shop.find_or_initialize_by(name: shop_name)
shop.token = token
shop.save!
session[:shopify] = {
shop: shop_name,
token: token
}
after_shopify_auth()
return_to = env['omniauth.params']['return_to']
return_params = session[:return_params]
session.delete(:return_params)
return_to += "?#{return_params.to_query}" if return_params.present?
redirect return_to
end
app.get '/auth/failure' do
erb "Authentication Failed:
message: #{params}
", layout: false
end
end
end
register Shopify
end
class Shop < ActiveRecord::Base
def self.secret
@secret ||= ENV['SECRET']
end
attr_encrypted :token,
key: secret,
attribute: 'token_encrypted',
mode: :single_iv_and_salt,
algorithm: 'aes-256-cbc',
insecure_mode: true
validates_presence_of :name
validates_presence_of :token, on: :create
end