loopstak-shopify-sinatra-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG +52 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +114 -0
- data/LICENSE +20 -0
- data/README.md +228 -0
- data/bin/shopify-sinatra-app-generator +82 -0
- data/example/.gitignore +3 -0
- data/example/Gemfile +28 -0
- data/example/Procfile +1 -0
- data/example/README.md +4 -0
- data/example/Rakefile +37 -0
- data/example/config.ru +7 -0
- data/example/config/database.yml +12 -0
- data/example/db/migrate/20140413221328_create_shops.rb +12 -0
- data/example/db/migrate/20140414042317_add_index_to_shops.rb +9 -0
- data/example/db/schema.rb +21 -0
- data/example/db/seeds.rb +1 -0
- data/example/public/icon.png +0 -0
- data/example/public/legend.gif +0 -0
- data/example/src/app.rb +54 -0
- data/example/test/app_test.rb +80 -0
- data/example/test/test_helper.rb +31 -0
- data/example/views/_flash_messages.erb +11 -0
- data/example/views/_top_bar.erb +7 -0
- data/example/views/home.erb +9 -0
- data/example/views/install.erb +40 -0
- data/example/views/layouts/application.erb +22 -0
- data/lib/sinatra/shopify-sinatra-app.rb +267 -0
- data/shopify-sinatra-app.gemspec +31 -0
- metadata +261 -0
data/example/.gitignore
ADDED
data/example/Gemfile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
ruby '2.5.3'
|
3
|
+
|
4
|
+
gem 'shopify-sinatra-app', path: '../'
|
5
|
+
gem 'sinatra-activerecord'
|
6
|
+
gem 'rack-flash3', require: 'rack-flash'
|
7
|
+
|
8
|
+
group :production do
|
9
|
+
gem 'pg'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :development, :test do
|
13
|
+
gem 'sqlite3'
|
14
|
+
gem 'byebug'
|
15
|
+
end
|
16
|
+
|
17
|
+
group :development do
|
18
|
+
gem 'rake'
|
19
|
+
gem 'foreman'
|
20
|
+
gem 'dotenv'
|
21
|
+
end
|
22
|
+
|
23
|
+
group :test do
|
24
|
+
gem 'mocha', require: false
|
25
|
+
gem 'minitest'
|
26
|
+
gem 'rack-test'
|
27
|
+
gem 'fakeweb'
|
28
|
+
end
|
data/example/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: bundle exec rackup config.ru -p $PORT
|
data/example/README.md
ADDED
data/example/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'sinatra/activerecord/rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require './src/app'
|
4
|
+
|
5
|
+
task :creds2heroku do
|
6
|
+
Bundler.with_clean_env do
|
7
|
+
File.readlines('.env').each do |var|
|
8
|
+
pipe = IO.popen("heroku config:set #{var}")
|
9
|
+
while (line = pipe.gets)
|
10
|
+
print line
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
task :deploy2heroku do
|
17
|
+
pipe = IO.popen('git push heroku master --force')
|
18
|
+
while (line = pipe.gets)
|
19
|
+
print line
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :test do
|
24
|
+
task :prepare do
|
25
|
+
`RACK_ENV=test rake db:create`
|
26
|
+
`RACK_ENV=test rake db:migrate`
|
27
|
+
`RACK_ENV=test SECRET=secret rake db:seed`
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
task :test do
|
32
|
+
Rake::TestTask.new do |t|
|
33
|
+
t.pattern = 'test/*_test.rb'
|
34
|
+
t.libs << 'test'
|
35
|
+
t.verbose = true
|
36
|
+
end
|
37
|
+
end
|
data/example/config.ru
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# Note that this schema.rb definition is the authoritative source for your
|
6
|
+
# database schema. If you need to create the application database on another
|
7
|
+
# system, you should be using db:schema:load, not running all the migrations
|
8
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
9
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(version: 2014_04_14_042317) do
|
14
|
+
|
15
|
+
create_table "shops", force: :cascade do |t|
|
16
|
+
t.string "name"
|
17
|
+
t.string "token_encrypted"
|
18
|
+
t.index ["name"], name: "index_shops_on_name"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/example/db/seeds.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
shop = Shop.create(name: 'testshop.myshopify.com', token: 'token')
|
Binary file
|
Binary file
|
data/example/src/app.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'sinatra/shopify-sinatra-app'
|
2
|
+
|
3
|
+
class SinatraApp < Sinatra::Base
|
4
|
+
register Sinatra::Shopify
|
5
|
+
|
6
|
+
# set the scope that your app needs, read more here:
|
7
|
+
# http://docs.shopify.com/api/tutorials/oauth
|
8
|
+
set :scope, 'read_products, read_orders'
|
9
|
+
|
10
|
+
# Your App's Home page
|
11
|
+
# this is a simple example that fetches some products
|
12
|
+
# from Shopify and displays them inside your app
|
13
|
+
get '/' do
|
14
|
+
shopify_session do |shop_name|
|
15
|
+
@shop = ShopifyAPI::Shop.current
|
16
|
+
@products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
|
17
|
+
erb :home
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# this endpoint recieves the uninstall webhook
|
22
|
+
# and cleans up data, add to this endpoint as your app
|
23
|
+
# stores more data.
|
24
|
+
post '/uninstall' do
|
25
|
+
shopify_webhook do |shop_name, params|
|
26
|
+
Shop.find_by(name: shop_name).destroy
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# This method gets called when your app is installed.
|
33
|
+
# setup any webhooks or services you need on Shopify
|
34
|
+
# inside here.
|
35
|
+
def after_shopify_auth
|
36
|
+
# shopify_session do
|
37
|
+
# create an uninstall webhook, this webhook gets sent
|
38
|
+
# when your app is uninstalled from a shop. It is good
|
39
|
+
# practice to clean up any data from a shop when they
|
40
|
+
# uninstall your app:
|
41
|
+
|
42
|
+
# uninstall_webhook = ShopifyAPI::Webhook.new(
|
43
|
+
# topic: 'app/uninstalled',
|
44
|
+
# address: "#{base_url}/uninstall",
|
45
|
+
# format: 'json'
|
46
|
+
# )
|
47
|
+
# begin
|
48
|
+
# uninstall_webhook.save!
|
49
|
+
# rescue => e
|
50
|
+
# raise unless uninstall_webhook.persisted?
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require './src/app'
|
3
|
+
|
4
|
+
class MockShop
|
5
|
+
def initialize(shop_name)
|
6
|
+
@shop_name = shop_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def myshopify_domain
|
10
|
+
@shop_name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class AppTest < Minitest::Test
|
15
|
+
def app
|
16
|
+
SinatraApp
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup
|
20
|
+
@shop_name = 'testshop.myshopify.com'
|
21
|
+
@shopify_shop = MockShop.new(@shop_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_root_with_session
|
25
|
+
set_session
|
26
|
+
fake 'https://testshop.myshopify.com/admin/shop.json', body: {myshopify_domain: @shop_name}.to_json
|
27
|
+
fake 'https://testshop.myshopify.com/admin/products.json?limit=10', body: '{}'
|
28
|
+
get '/'
|
29
|
+
assert last_response.ok?
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_root_with_session_activates_api
|
33
|
+
set_session
|
34
|
+
SinatraApp.any_instance.expects(:activate_shopify_api).with(@shop_name, 'token')
|
35
|
+
ShopifyAPI::Shop.expects(:current).returns(@shopify_shop)
|
36
|
+
ShopifyAPI::Product.expects(:find).returns([])
|
37
|
+
get '/'
|
38
|
+
assert last_response.ok?
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_root_without_session_redirects_to_install
|
42
|
+
get '/'
|
43
|
+
assert_equal 302, last_response.status
|
44
|
+
assert_equal 'http://example.org/install', last_response.location
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_root_with_shop_redirects_to_auth
|
48
|
+
get '/?shop=othertestshop.myshopify.com'
|
49
|
+
assert_match '/auth/shopify?shop=othertestshop.myshopify.com', last_response.body
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_root_with_session_and_new_shop_redirects_to_auth
|
53
|
+
set_session
|
54
|
+
get '/?shop=othertestshop.myshopify.com'
|
55
|
+
assert_match '/auth/shopify?shop=othertestshop.myshopify.com', last_response.body
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_root_rescues_UnauthorizedAccess_clears_session_and_redirects
|
59
|
+
set_session
|
60
|
+
SinatraApp.any_instance.expects(:activate_shopify_api).with(@shop_name, 'token')
|
61
|
+
SinatraApp.any_instance.expects(:clear_session)
|
62
|
+
ShopifyAPI::Shop.expects(:current).raises(ActiveResource::UnauthorizedAccess.new('UnauthorizedAccess'))
|
63
|
+
get '/'
|
64
|
+
assert_equal 302, last_response.status
|
65
|
+
assert_equal 'http://example.org/', last_response.location
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_uninstall_webhook_endpoint
|
69
|
+
SinatraApp.any_instance.expects(:verify_shopify_webhook).returns(true)
|
70
|
+
Shop.any_instance.expects(:destroy)
|
71
|
+
post '/uninstall', '{}', 'HTTP_X_SHOPIFY_SHOP_DOMAIN' => @shop_name
|
72
|
+
assert last_response.ok?
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def set_session(shop = 'testshop.myshopify.com', token = 'token')
|
78
|
+
SinatraApp.any_instance.stubs(:session).returns(shopify: { shop: shop, token: token })
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
$VERBOSE = nil
|
2
|
+
|
3
|
+
ENV['RACK_ENV'] = 'test'
|
4
|
+
ENV['SECRET'] = 'secret'
|
5
|
+
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'rack/test'
|
8
|
+
require 'mocha/setup'
|
9
|
+
require 'fakeweb'
|
10
|
+
|
11
|
+
FakeWeb.allow_net_connect = false
|
12
|
+
|
13
|
+
module Helpers
|
14
|
+
include Rack::Test::Methods
|
15
|
+
|
16
|
+
def load_fixture(name)
|
17
|
+
File.read("./test/fixtures/#{name}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def fake(url, options = {})
|
21
|
+
method = options.delete(:method) || :get
|
22
|
+
body = options.delete(:body) || '{}'
|
23
|
+
format = options.delete(:format) || :json
|
24
|
+
|
25
|
+
FakeWeb.register_uri(method, url, { body: body, status: 200, content_type: "application/#{format}" }.merge(options))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Minitest::Test
|
30
|
+
include Helpers
|
31
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<script type="text/javascript">
|
2
|
+
ShopifyApp.ready(function(){
|
3
|
+
<% if flash[:notice] %>
|
4
|
+
ShopifyApp.flashNotice("<%= flash[:notice] %>");
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<% if flash[:error] %>
|
8
|
+
ShopifyApp.flashError("<%= flash[:error] %>");
|
9
|
+
<% end %>
|
10
|
+
});
|
11
|
+
</script>
|
@@ -0,0 +1,40 @@
|
|
1
|
+
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
|
2
|
+
|
3
|
+
<style>
|
4
|
+
body {
|
5
|
+
background: #fff url(<%="#{base_url}/legend.gif"%>) no-repeat fixed right 130px;
|
6
|
+
}
|
7
|
+
|
8
|
+
.form-large input {
|
9
|
+
font-size: 18px;
|
10
|
+
padding: 8px;
|
11
|
+
height: auto;
|
12
|
+
line-height: normal;
|
13
|
+
}
|
14
|
+
|
15
|
+
.form-wide {
|
16
|
+
width: 480px;
|
17
|
+
}
|
18
|
+
</style>
|
19
|
+
|
20
|
+
<body>
|
21
|
+
<div style="margin-top: 100px"></div>
|
22
|
+
|
23
|
+
<div class="container">
|
24
|
+
<h1>Shopify Sinatra App</h1>
|
25
|
+
<h2><small>This app requires you to login to start using it.</small></h2>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div style="margin-top: 30px"></div>
|
29
|
+
|
30
|
+
<div class="container">
|
31
|
+
<form role="form" class="form-large" action="/login" method="post">
|
32
|
+
<div class="input-group form-wide">
|
33
|
+
<input class="form-control" type="url" name="shop" placeholder="Shop URL">
|
34
|
+
<span class="input-group-btn">
|
35
|
+
<input class="btn" type="submit" value="Install App" />
|
36
|
+
</span>
|
37
|
+
</div>
|
38
|
+
</form>
|
39
|
+
</div>
|
40
|
+
</body>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<script src="https://cdn.shopify.com/s/assets/external/app.js"></script>
|
5
|
+
<script type="text/javascript">
|
6
|
+
ShopifyApp.init({
|
7
|
+
apiKey: "<%= SinatraApp.settings.api_key %>",
|
8
|
+
shopOrigin: "<%= shop_origin %>",
|
9
|
+
debug: true
|
10
|
+
});
|
11
|
+
</script>
|
12
|
+
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body>
|
16
|
+
<div class="container">
|
17
|
+
<%= erb :'_top_bar', layout: false, locals: locals %>
|
18
|
+
<%= erb :'_flash_messages', layout: false, locals: locals %>
|
19
|
+
<%= yield %>
|
20
|
+
</div>
|
21
|
+
</body>
|
22
|
+
</html>
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/activerecord'
|
3
|
+
|
4
|
+
require 'rack-flash'
|
5
|
+
require 'attr_encrypted'
|
6
|
+
require 'active_support/all'
|
7
|
+
|
8
|
+
require 'shopify_api'
|
9
|
+
require 'omniauth-shopify-oauth2'
|
10
|
+
|
11
|
+
module Sinatra
|
12
|
+
module Shopify
|
13
|
+
module Methods
|
14
|
+
|
15
|
+
# designed to be overriden
|
16
|
+
def after_shopify_auth
|
17
|
+
end
|
18
|
+
|
19
|
+
def logout
|
20
|
+
session.delete(:shopify)
|
21
|
+
session.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
# for the esdk initializer
|
25
|
+
def shop_origin
|
26
|
+
"https://#{session[:shopify][:shop]}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def shopify_session(&blk)
|
30
|
+
return_to = request.path
|
31
|
+
return_params = request.params
|
32
|
+
|
33
|
+
if no_session?
|
34
|
+
authenticate(return_to, return_params)
|
35
|
+
elsif different_shop?
|
36
|
+
logout
|
37
|
+
authenticate(return_to, return_params)
|
38
|
+
else
|
39
|
+
shop_name = session[:shopify][:shop]
|
40
|
+
token = session[:shopify][:token]
|
41
|
+
activate_shopify_api(shop_name, token)
|
42
|
+
yield shop_name
|
43
|
+
end
|
44
|
+
rescue ActiveResource::UnauthorizedAccess
|
45
|
+
clear_session shop_name
|
46
|
+
redirect request.path
|
47
|
+
end
|
48
|
+
|
49
|
+
def shopify_webhook(&blk)
|
50
|
+
return unless verify_shopify_webhook
|
51
|
+
shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
|
52
|
+
webhook_body = ActiveSupport::JSON.decode(request.body.read.to_s)
|
53
|
+
yield shop_name, webhook_body
|
54
|
+
status 200
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def request_protocol
|
60
|
+
request.secure? ? 'https' : 'http'
|
61
|
+
end
|
62
|
+
|
63
|
+
def base_url
|
64
|
+
"#{request_protocol}://#{request.env['HTTP_HOST']}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def no_session?
|
68
|
+
!session.key?(:shopify)
|
69
|
+
end
|
70
|
+
|
71
|
+
def different_shop?
|
72
|
+
params[:shop].present? && session[:shopify][:shop] != sanitize_shop_param(params)
|
73
|
+
end
|
74
|
+
|
75
|
+
def authenticate(return_to = '/', return_params = nil)
|
76
|
+
if shop_name = sanitized_shop_name
|
77
|
+
session[:return_params] = return_params if return_params
|
78
|
+
redirect_url = "/auth/shopify?shop=#{shop_name}&return_to=#{base_url}#{return_to}"
|
79
|
+
redirect_javascript redirect_url
|
80
|
+
else
|
81
|
+
redirect '/install'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def activate_shopify_api(shop_name, token)
|
86
|
+
api_session = ShopifyAPI::Session.new(domain: shop_name, token: token, api_version: ENV['SHOPIFY_API_VERSION'] || '2020-01')
|
87
|
+
ShopifyAPI::Base.activate_session(api_session)
|
88
|
+
end
|
89
|
+
|
90
|
+
def clear_session(shop_name)
|
91
|
+
logout
|
92
|
+
shop = Shop.find_by(name: shop_name)
|
93
|
+
shop.token = nil
|
94
|
+
shop.save
|
95
|
+
end
|
96
|
+
|
97
|
+
def redirect_javascript(url)
|
98
|
+
erb %(
|
99
|
+
<!DOCTYPE html>
|
100
|
+
<html lang="en">
|
101
|
+
<head>
|
102
|
+
<meta charset="utf-8" />
|
103
|
+
<base target="_top">
|
104
|
+
<title>Redirecting…</title>
|
105
|
+
|
106
|
+
<script type='text/javascript'>
|
107
|
+
// If the current window is the 'parent', change the URL by setting location.href
|
108
|
+
if (window.top == window.self) {
|
109
|
+
window.top.location.href = #{url.to_json};
|
110
|
+
|
111
|
+
// If the current window is the 'child', change the parent's URL with postMessage
|
112
|
+
} else {
|
113
|
+
message = JSON.stringify({
|
114
|
+
message: 'Shopify.API.remoteRedirect',
|
115
|
+
data: { location: window.location.origin + #{url.to_json} }
|
116
|
+
});
|
117
|
+
window.parent.postMessage(message, 'https://#{sanitized_shop_name}');
|
118
|
+
}
|
119
|
+
</script>
|
120
|
+
</head>
|
121
|
+
<body>
|
122
|
+
</body>
|
123
|
+
</html>
|
124
|
+
), layout: false
|
125
|
+
end
|
126
|
+
|
127
|
+
def sanitized_shop_name
|
128
|
+
@sanitized_shop_name ||= sanitize_shop_param(params)
|
129
|
+
end
|
130
|
+
|
131
|
+
def sanitize_shop_param(params)
|
132
|
+
return unless params[:shop].present?
|
133
|
+
name = params[:shop].to_s.strip
|
134
|
+
name += '.myshopify.com' if !name.include?('myshopify.com') && !name.include?('.')
|
135
|
+
name.gsub!('https://', '')
|
136
|
+
name.gsub!('http://', '')
|
137
|
+
|
138
|
+
u = URI("http://#{name}")
|
139
|
+
u.host.ends_with?('.myshopify.com') ? u.host : nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def verify_shopify_webhook
|
143
|
+
data = request.body.read.to_s
|
144
|
+
digest = OpenSSL::Digest.new('sha256')
|
145
|
+
calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, settings.shared_secret, data)).strip
|
146
|
+
request.body.rewind
|
147
|
+
|
148
|
+
if calculated_hmac == request.env['HTTP_X_SHOPIFY_HMAC_SHA256']
|
149
|
+
true
|
150
|
+
else
|
151
|
+
puts 'Shopify Webhook verifictation failed!'
|
152
|
+
false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.registered(app)
|
158
|
+
app.helpers Shopify::Methods
|
159
|
+
app.register Sinatra::ActiveRecordExtension
|
160
|
+
|
161
|
+
app.set :database_file, File.expand_path('config/database.yml')
|
162
|
+
app.set :views, File.expand_path('views')
|
163
|
+
app.set :public_folder, File.expand_path('public')
|
164
|
+
app.set :erb, layout: :'layouts/application'
|
165
|
+
app.set :protection, except: :frame_options
|
166
|
+
|
167
|
+
app.enable :sessions
|
168
|
+
app.enable :inline_templates
|
169
|
+
|
170
|
+
app.set :scope, 'read_products, read_orders'
|
171
|
+
|
172
|
+
app.set :api_key, ENV['SHOPIFY_API_KEY']
|
173
|
+
app.set :shared_secret, ENV['SHOPIFY_SHARED_SECRET']
|
174
|
+
app.set :secret, ENV['SECRET']
|
175
|
+
|
176
|
+
app.use Rack::Flash, sweep: true
|
177
|
+
app.use Rack::MethodOverride
|
178
|
+
app.use Rack::Session::Cookie, key: 'rack.session',
|
179
|
+
path: '/',
|
180
|
+
secret: app.settings.secret,
|
181
|
+
expire_after: 60 * 30 # half an hour in seconds
|
182
|
+
|
183
|
+
app.use OmniAuth::Builder do
|
184
|
+
provider :shopify,
|
185
|
+
app.settings.api_key,
|
186
|
+
app.settings.shared_secret,
|
187
|
+
|
188
|
+
scope: app.settings.scope,
|
189
|
+
|
190
|
+
setup: lambda { |env|
|
191
|
+
params = Rack::Utils.parse_query(env['QUERY_STRING'])
|
192
|
+
site_url = "https://#{params['shop']}"
|
193
|
+
env['omniauth.strategy'].options[:client_options][:site] = site_url
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
ShopifyAPI::Session.setup(
|
198
|
+
api_key: app.settings.api_key,
|
199
|
+
secret: app.settings.shared_secret
|
200
|
+
)
|
201
|
+
|
202
|
+
app.get '/install' do
|
203
|
+
if params[:shop].present?
|
204
|
+
authenticate
|
205
|
+
else
|
206
|
+
erb :install, layout: false
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
app.post '/login' do
|
211
|
+
authenticate
|
212
|
+
end
|
213
|
+
|
214
|
+
app.get '/logout' do
|
215
|
+
logout
|
216
|
+
redirect '/install'
|
217
|
+
end
|
218
|
+
|
219
|
+
app.get '/auth/shopify/callback' do
|
220
|
+
shop_name = params['shop']
|
221
|
+
token = request.env['omniauth.auth']['credentials']['token']
|
222
|
+
|
223
|
+
shop = Shop.find_or_initialize_by(name: shop_name)
|
224
|
+
shop.token = token
|
225
|
+
shop.save!
|
226
|
+
|
227
|
+
session[:shopify] = {
|
228
|
+
shop: shop_name,
|
229
|
+
token: token
|
230
|
+
}
|
231
|
+
|
232
|
+
after_shopify_auth()
|
233
|
+
|
234
|
+
return_to = env['omniauth.params']['return_to']
|
235
|
+
return_params = session[:return_params]
|
236
|
+
session.delete(:return_params)
|
237
|
+
|
238
|
+
return_to += "?#{return_params.to_query}" if return_params.present?
|
239
|
+
|
240
|
+
redirect return_to
|
241
|
+
end
|
242
|
+
|
243
|
+
app.get '/auth/failure' do
|
244
|
+
erb "<h1>Authentication Failed:</h1>
|
245
|
+
<h3>message:<h3> <pre>#{params}</pre>", layout: false
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
register Shopify
|
251
|
+
end
|
252
|
+
|
253
|
+
class Shop < ActiveRecord::Base
|
254
|
+
def self.secret
|
255
|
+
@secret ||= ENV['SECRET']
|
256
|
+
end
|
257
|
+
|
258
|
+
attr_encrypted :token,
|
259
|
+
key: secret,
|
260
|
+
attribute: 'token_encrypted',
|
261
|
+
mode: :single_iv_and_salt,
|
262
|
+
algorithm: 'aes-256-cbc',
|
263
|
+
insecure_mode: true
|
264
|
+
|
265
|
+
validates_presence_of :name
|
266
|
+
validates_presence_of :token, on: :create
|
267
|
+
end
|