spiffy_stores_app 8.2.6

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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rubocop.yml +7 -0
  4. data/.travis.yml +9 -0
  5. data/CHANGELOG.md +3 -0
  6. data/Gemfile +6 -0
  7. data/README.md +346 -0
  8. data/Rakefile +6 -0
  9. data/app/assets/javascripts/spiffy_stores_app/redirect.js +19 -0
  10. data/app/controllers/spiffy_stores_app/authenticated_controller.rb +11 -0
  11. data/app/controllers/spiffy_stores_app/sessions_controller.rb +113 -0
  12. data/app/controllers/spiffy_stores_app/webhooks_controller.rb +36 -0
  13. data/app/views/spiffy_stores_app/sessions/new.html.erb +123 -0
  14. data/app/views/spiffy_stores_app/shared/redirect.html.erb +22 -0
  15. data/config/locales/de.yml +3 -0
  16. data/config/locales/en.yml +4 -0
  17. data/config/locales/es.yml +3 -0
  18. data/config/locales/fr.yml +4 -0
  19. data/config/locales/ja.yml +3 -0
  20. data/config/routes.rb +12 -0
  21. data/docs/Quickstart.md +76 -0
  22. data/images/app-proxy-screenshot.png +0 -0
  23. data/lib/generators/spiffy_stores_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +43 -0
  24. data/lib/generators/spiffy_stores_app/add_after_authenticate_job/templates/after_authenticate_job.rb +10 -0
  25. data/lib/generators/spiffy_stores_app/add_webhook/add_webhook_generator.rb +68 -0
  26. data/lib/generators/spiffy_stores_app/add_webhook/templates/webhook_job.rb +8 -0
  27. data/lib/generators/spiffy_stores_app/app_proxy_controller/app_proxy_controller_generator.rb +25 -0
  28. data/lib/generators/spiffy_stores_app/app_proxy_controller/templates/app_proxy_controller.rb +8 -0
  29. data/lib/generators/spiffy_stores_app/app_proxy_controller/templates/app_proxy_route.rb +10 -0
  30. data/lib/generators/spiffy_stores_app/app_proxy_controller/templates/index.html.erb +19 -0
  31. data/lib/generators/spiffy_stores_app/controllers/controllers_generator.rb +29 -0
  32. data/lib/generators/spiffy_stores_app/home_controller/home_controller_generator.rb +31 -0
  33. data/lib/generators/spiffy_stores_app/home_controller/templates/home_controller.rb +6 -0
  34. data/lib/generators/spiffy_stores_app/home_controller/templates/index.html.erb +21 -0
  35. data/lib/generators/spiffy_stores_app/home_controller/templates/spiffy_stores_app_ready_script.html.erb +7 -0
  36. data/lib/generators/spiffy_stores_app/install/install_generator.rb +58 -0
  37. data/lib/generators/spiffy_stores_app/install/templates/_flash_messages.html.erb +19 -0
  38. data/lib/generators/spiffy_stores_app/install/templates/embedded_app.html.erb +40 -0
  39. data/lib/generators/spiffy_stores_app/install/templates/omniauth.rb +2 -0
  40. data/lib/generators/spiffy_stores_app/install/templates/spiffy_provider.rb +11 -0
  41. data/lib/generators/spiffy_stores_app/install/templates/spiffy_stores_app.rb +9 -0
  42. data/lib/generators/spiffy_stores_app/routes/routes_generator.rb +31 -0
  43. data/lib/generators/spiffy_stores_app/routes/templates/routes.rb +11 -0
  44. data/lib/generators/spiffy_stores_app/shop_model/shop_model_generator.rb +38 -0
  45. data/lib/generators/spiffy_stores_app/shop_model/templates/db/migrate/create_shops.erb +15 -0
  46. data/lib/generators/spiffy_stores_app/shop_model/templates/shop.rb +3 -0
  47. data/lib/generators/spiffy_stores_app/shop_model/templates/shops.yml +3 -0
  48. data/lib/generators/spiffy_stores_app/spiffy_stores_app_generator.rb +16 -0
  49. data/lib/generators/spiffy_stores_app/views/views_generator.rb +29 -0
  50. data/lib/spiffy_stores_app.rb +34 -0
  51. data/lib/spiffy_stores_app/configuration.rb +72 -0
  52. data/lib/spiffy_stores_app/controller_concerns/app_proxy_verification.rb +38 -0
  53. data/lib/spiffy_stores_app/controller_concerns/embedded_app.rb +19 -0
  54. data/lib/spiffy_stores_app/controller_concerns/localization.rb +22 -0
  55. data/lib/spiffy_stores_app/controller_concerns/login_protection.rb +103 -0
  56. data/lib/spiffy_stores_app/controller_concerns/webhook_verification.rb +34 -0
  57. data/lib/spiffy_stores_app/engine.rb +10 -0
  58. data/lib/spiffy_stores_app/jobs/scripttags_manager_job.rb +15 -0
  59. data/lib/spiffy_stores_app/jobs/webhooks_manager_job.rb +15 -0
  60. data/lib/spiffy_stores_app/managers/scripttags_manager.rb +77 -0
  61. data/lib/spiffy_stores_app/managers/webhooks_manager.rb +61 -0
  62. data/lib/spiffy_stores_app/session/in_memory_session_store.rb +27 -0
  63. data/lib/spiffy_stores_app/session/session_repository.rb +34 -0
  64. data/lib/spiffy_stores_app/session/session_storage.rb +32 -0
  65. data/lib/spiffy_stores_app/utils.rb +16 -0
  66. data/lib/spiffy_stores_app/version.rb +3 -0
  67. data/spiffy_stores_app.gemspec +26 -0
  68. metadata +220 -0
@@ -0,0 +1,34 @@
1
+ require 'spiffy_stores_app/version'
2
+
3
+ # deps
4
+ require 'spiffy_stores_api'
5
+ require 'omniauth-spiffy-oauth2'
6
+
7
+ # config
8
+ require 'spiffy_stores_app/configuration'
9
+
10
+ # engine
11
+ require 'spiffy_stores_app/engine'
12
+
13
+ # utils
14
+ require 'spiffy_stores_app/utils'
15
+
16
+ # controller concerns
17
+ require 'spiffy_stores_app/controller_concerns/localization'
18
+ require 'spiffy_stores_app/controller_concerns/login_protection'
19
+ require 'spiffy_stores_app/controller_concerns/embedded_app'
20
+ require 'spiffy_stores_app/controller_concerns/webhook_verification'
21
+ require 'spiffy_stores_app/controller_concerns/app_proxy_verification'
22
+
23
+ # jobs
24
+ require 'spiffy_stores_app/jobs/webhooks_manager_job'
25
+ require 'spiffy_stores_app/jobs/scripttags_manager_job'
26
+
27
+ # mangers
28
+ require 'spiffy_stores_app/managers/webhooks_manager'
29
+ require 'spiffy_stores_app/managers/scripttags_manager'
30
+
31
+ # session
32
+ require 'spiffy_stores_app/session/session_storage'
33
+ require 'spiffy_stores_app/session/session_repository'
34
+ require 'spiffy_stores_app/session/in_memory_session_store'
@@ -0,0 +1,72 @@
1
+ module SpiffyStoresApp
2
+ class Configuration
3
+
4
+ # SpiffyStores App settings. These values should match the configuration
5
+ # for the app in your SpiffyStores Partners page. Change your settings in
6
+ # `config/initializers/spiffy_stores_app.rb`
7
+ attr_accessor :application_name
8
+ attr_accessor :api_key
9
+ attr_accessor :secret
10
+ attr_accessor :scope
11
+ attr_accessor :embedded_app
12
+ alias_method :embedded_app?, :embedded_app
13
+ attr_accessor :webhooks
14
+ attr_accessor :scripttags
15
+ attr_accessor :after_authenticate_job
16
+ attr_accessor :session_repository
17
+
18
+ # customise urls
19
+ attr_accessor :root_url
20
+
21
+ # customise ActiveJob queue names
22
+ attr_accessor :scripttags_manager_queue_name
23
+ attr_accessor :webhooks_manager_queue_name
24
+
25
+ # configure spiffystores domain for local spiffy_stores development
26
+ attr_accessor :spiffy_stores_domain
27
+
28
+ # allow namespacing webhook jobs
29
+ attr_accessor :webhook_jobs_namespace
30
+
31
+ def initialize
32
+ @root_url = '/'
33
+ @spiffy_stores_domain = 'spiffystores.com'
34
+ @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
35
+ @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
36
+ end
37
+
38
+ def login_url
39
+ File.join(@root_url, 'login')
40
+ end
41
+
42
+ def session_repository=(klass)
43
+ if Rails.configuration.cache_classes
44
+ SpiffyStoresApp::SessionRepository.storage = klass
45
+ else
46
+ ActiveSupport::Reloader.to_prepare do
47
+ SpiffyStoresApp::SessionRepository.storage = klass
48
+ end
49
+ end
50
+ end
51
+
52
+ def has_webhooks?
53
+ webhooks.present?
54
+ end
55
+
56
+ def has_scripttags?
57
+ scripttags.present?
58
+ end
59
+ end
60
+
61
+ def self.configuration
62
+ @configuration ||= Configuration.new
63
+ end
64
+
65
+ def self.configuration=(config)
66
+ @configuration = config
67
+ end
68
+
69
+ def self.configure
70
+ yield configuration
71
+ end
72
+ end
@@ -0,0 +1,38 @@
1
+ module SpiffyStoresApp
2
+ module AppProxyVerification
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ skip_before_action :verify_authenticity_token, raise: false
7
+ before_action :verify_proxy_request
8
+ end
9
+
10
+ def verify_proxy_request
11
+ return head :forbidden unless query_string_valid?(request.query_string)
12
+ end
13
+
14
+ private
15
+
16
+ def query_string_valid?(query_string)
17
+ query_hash = Rack::Utils.parse_query(query_string)
18
+
19
+ signature = query_hash.delete('signature')
20
+ return false if signature.nil?
21
+
22
+ ActiveSupport::SecurityUtils.secure_compare(
23
+ calculated_signature(query_hash),
24
+ signature
25
+ )
26
+ end
27
+
28
+ def calculated_signature(query_hash_without_signature)
29
+ sorted_params = query_hash_without_signature.collect{|k,v| "#{k}=#{Array(v).join(',')}"}.sort.join
30
+
31
+ OpenSSL::HMAC.hexdigest(
32
+ OpenSSL::Digest.new('sha256'),
33
+ SpiffyStoresApp.configuration.secret,
34
+ sorted_params
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ module SpiffyStoresApp
2
+ module EmbeddedApp
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ if SpiffyStoresApp.configuration.embedded_app?
7
+ after_action :set_esdk_headers
8
+ layout 'embedded_app'
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def set_esdk_headers
15
+ response.set_header('P3P', 'CP="Not used"')
16
+ response.headers.except!('X-Frame-Options')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module SpiffyStoresApp
2
+ module Localization
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :set_locale
7
+ end
8
+
9
+ private
10
+
11
+ def set_locale
12
+ if params[:locale]
13
+ session[:locale] = params[:locale]
14
+ else
15
+ session[:locale] ||= I18n.default_locale
16
+ end
17
+ I18n.locale = session[:locale]
18
+ rescue I18n::InvalidLocale
19
+ I18n.locale = I18n.default_locale
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,103 @@
1
+ module SpiffyStoresApp
2
+ module LoginProtection
3
+ extend ActiveSupport::Concern
4
+
5
+ class SpiffyStoresDomainNotFound < StandardError; end
6
+
7
+ included do
8
+ rescue_from ActiveResource::UnauthorizedAccess, :with => :close_session
9
+ end
10
+
11
+ def spiffy_stores_session
12
+ if shop_session
13
+ begin
14
+ SpiffyStoresAPI::Base.activate_session(shop_session)
15
+ yield
16
+ ensure
17
+ SpiffyStoresAPI::Base.clear_session
18
+ end
19
+ else
20
+ redirect_to_login
21
+ end
22
+ end
23
+
24
+ def shop_session
25
+ return unless session[:spiffy_stores]
26
+ @shop_session ||= SpiffyStoresApp::SessionRepository.retrieve(session[:spiffy_stores])
27
+ end
28
+
29
+ def login_again_if_different_shop
30
+ if shop_session && params[:store] && params[:store].is_a?(String) && shop_session.url != params[:store]
31
+ clear_shop_session
32
+ redirect_to_login
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def redirect_to_login
39
+ if request.xhr?
40
+ head :unauthorized
41
+ else
42
+ if request.get?
43
+ session[:return_to] = "#{request.path}?#{sanitized_params.to_query}"
44
+ end
45
+ redirect_to login_url
46
+ end
47
+ end
48
+
49
+ def close_session
50
+ clear_shop_session
51
+ redirect_to login_url
52
+ end
53
+
54
+ def clear_shop_session
55
+ session[:spiffy_stores] = nil
56
+ session[:spiffy_stores_domain] = nil
57
+ session[:spiffy_stores_user] = nil
58
+ end
59
+
60
+ def login_url
61
+ url = SpiffyStoresApp.configuration.login_url
62
+
63
+ if params[:store].present?
64
+ query = { store: sanitized_params[:store] }.to_query
65
+ url = "#{url}?#{query}"
66
+ end
67
+
68
+ url
69
+ end
70
+
71
+ def fullpage_redirect_to(url)
72
+ if SpiffyStoresApp.configuration.embedded_app?
73
+ render 'spiffy_stores_app/shared/redirect', layout: false, locals: { url: url, current_spiffy_stores_domain: current_spiffy_stores_domain }
74
+ else
75
+ redirect_to url
76
+ end
77
+ end
78
+
79
+ def current_spiffy_stores_domain
80
+ spiffy_stores_domain = sanitized_shop_name || session[:spiffy_stores_domain]
81
+ return spiffy_stores_domain if spiffy_stores_domain.present?
82
+
83
+ raise SpiffyStoresDomainNotFound
84
+ end
85
+
86
+ def sanitized_shop_name
87
+ @sanitized_shop_name ||= sanitize_shop_param(params)
88
+ end
89
+
90
+ def sanitize_shop_param(params)
91
+ return unless params[:store].present?
92
+ SpiffyStoresApp::Utils.sanitize_shop_domain(params[:store])
93
+ end
94
+
95
+ def sanitized_params
96
+ request.query_parameters.clone.tap do |query_params|
97
+ if params[:store].is_a?(String)
98
+ query_params[:store] = sanitize_shop_param(params)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,34 @@
1
+ module SpiffyStoresApp
2
+ module WebhookVerification
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ skip_before_action :verify_authenticity_token, raise: false
7
+ before_action :verify_request
8
+ end
9
+
10
+ private
11
+
12
+ def verify_request
13
+ data = request.raw_post
14
+ return head :unauthorized unless hmac_valid?(data)
15
+ end
16
+
17
+ def hmac_valid?(data)
18
+ secret = SpiffyStoresApp.configuration.secret
19
+ digest = OpenSSL::Digest.new('sha256')
20
+ ActiveSupport::SecurityUtils.secure_compare(
21
+ spiffy_stores_hmac,
22
+ Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data)).strip
23
+ )
24
+ end
25
+
26
+ def shop_domain
27
+ request.headers['HTTP_X_SPIFFY_STORES_SHOP_DOMAIN']
28
+ end
29
+
30
+ def spiffy_stores_hmac
31
+ request.headers['HTTP_X_SPIFFY_STORES_HMAC_SHA256']
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module SpiffyStoresApp
2
+ class Engine < Rails::Engine
3
+ engine_name 'spiffy_stores_app'
4
+ isolate_namespace SpiffyStoresApp
5
+
6
+ initializer "spiffy_stores_app.assets.precompile" do |app|
7
+ app.config.assets.precompile += %w( spiffy_stores_app/redirect.js )
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module SpiffyStoresApp
2
+ class ScripttagsManagerJob < ActiveJob::Base
3
+
4
+ queue_as do
5
+ SpiffyStoresApp.configuration.scripttags_manager_queue_name
6
+ end
7
+
8
+ def perform(shop_domain:, shop_token:, scripttags:)
9
+ SpiffyStoresAPI::Session.temp(shop_domain, shop_token) do
10
+ manager = ScripttagsManager.new(scripttags, shop_domain)
11
+ manager.create_scripttags
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module SpiffyStoresApp
2
+ class WebhooksManagerJob < ActiveJob::Base
3
+
4
+ queue_as do
5
+ SpiffyStoresApp.configuration.webhooks_manager_queue_name
6
+ end
7
+
8
+ def perform(shop_domain:, shop_token:, webhooks:)
9
+ SpiffyStoresAPI::Session.temp(shop_domain, shop_token) do
10
+ manager = WebhooksManager.new(webhooks)
11
+ manager.create_webhooks
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,77 @@
1
+ module SpiffyStoresApp
2
+ class ScripttagsManager
3
+ class CreationFailed < StandardError; end
4
+
5
+ def self.queue(shop_domain, shop_token, scripttags)
6
+ SpiffyStoresApp::ScripttagsManagerJob.perform_later(
7
+ shop_domain: shop_domain,
8
+ shop_token: shop_token,
9
+ # Procs cannot be serialized so we interpolate now, if necessary
10
+ scripttags: build_src(scripttags, shop_domain)
11
+ )
12
+ end
13
+
14
+ def self.build_src(scripttags, domain)
15
+ scripttags.map do |tag|
16
+ next tag unless tag[:src].respond_to?(:call)
17
+ tag = tag.dup
18
+ tag[:src] = tag[:src].call(domain)
19
+ tag
20
+ end
21
+ end
22
+
23
+ attr_reader :required_scripttags, :shop_domain
24
+
25
+ def initialize(scripttags, shop_domain)
26
+ @required_scripttags = scripttags
27
+ @shop_domain = shop_domain
28
+ end
29
+
30
+ def recreate_scripttags!
31
+ destroy_scripttags
32
+ create_scripttags
33
+ end
34
+
35
+ def create_scripttags
36
+ return unless required_scripttags.present?
37
+
38
+ expanded_scripttags.each do |scripttag|
39
+ create_scripttag(scripttag) unless scripttag_exists?(scripttag[:src])
40
+ end
41
+ end
42
+
43
+ def destroy_scripttags
44
+ scripttags = expanded_scripttags
45
+ SpiffyStoresAPI::ScriptTag.all.each do |tag|
46
+ SpiffyStoresAPI::ScriptTag.delete(tag.id) if is_required_scripttag?(scripttags, tag)
47
+ end
48
+
49
+ @current_scripttags = nil
50
+ end
51
+
52
+ private
53
+
54
+ def expanded_scripttags
55
+ self.class.build_src(required_scripttags, shop_domain)
56
+ end
57
+
58
+ def is_required_scripttag?(scripttags, tag)
59
+ scripttags.map{ |w| w[:src] }.include? tag.src
60
+ end
61
+
62
+ def create_scripttag(attributes)
63
+ attributes.reverse_merge!(format: 'json')
64
+ scripttag = SpiffyStoresAPI::ScriptTag.create(attributes)
65
+ raise CreationFailed, scripttag.errors.full_messages.to_sentence unless scripttag.persisted?
66
+ scripttag
67
+ end
68
+
69
+ def scripttag_exists?(src)
70
+ current_scripttags[src]
71
+ end
72
+
73
+ def current_scripttags
74
+ @current_scripttags ||= SpiffyStoresAPI::ScriptTag.all.index_by(&:src)
75
+ end
76
+ end
77
+ end