loopstak-shopify-sinatra-app 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|