duodealer_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.
Files changed (212) hide show
  1. checksums.yaml +7 -0
  2. data/.!66854!duodealer_app.gemspec +0 -0
  3. data/.babelrc +5 -0
  4. data/.gitignore +16 -0
  5. data/.nvmrc +1 -0
  6. data/.rubocop.yml +263 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +27 -0
  9. data/Gemfile +8 -0
  10. data/LICENSE +19 -0
  11. data/README.md +553 -0
  12. data/Rakefile +6 -0
  13. data/app/assets/images/storage_access.svg +2 -0
  14. data/app/assets/javascripts/duodealer_app/enable_cookies.js +3 -0
  15. data/app/assets/javascripts/duodealer_app/itp_helper.js +40 -0
  16. data/app/assets/javascripts/duodealer_app/partition_cookies.js +8 -0
  17. data/app/assets/javascripts/duodealer_app/redirect.js +33 -0
  18. data/app/assets/javascripts/duodealer_app/request_storage_access.js +3 -0
  19. data/app/assets/javascripts/duodealer_app/storage_access.js +153 -0
  20. data/app/assets/javascripts/duodealer_app/storage_access_redirect.js +17 -0
  21. data/app/assets/javascripts/duodealer_app/top_level.js +2 -0
  22. data/app/assets/javascripts/duodealer_app/top_level_interaction.js +11 -0
  23. data/app/controllers/concerns/duodealer_app/authenticated.rb +15 -0
  24. data/app/controllers/concerns/duodealer_app/authenticated.rb-e +15 -0
  25. data/app/controllers/duodealer_app/authenticated_controller.rb +9 -0
  26. data/app/controllers/duodealer_app/authenticated_controller.rb-e +9 -0
  27. data/app/controllers/duodealer_app/callback_controller.rb +104 -0
  28. data/app/controllers/duodealer_app/callback_controller.rb-e +104 -0
  29. data/app/controllers/duodealer_app/extension_verification_controller.rb +19 -0
  30. data/app/controllers/duodealer_app/extension_verification_controller.rb-e +19 -0
  31. data/app/controllers/duodealer_app/sessions_controller.rb +159 -0
  32. data/app/controllers/duodealer_app/sessions_controller.rb-e +159 -0
  33. data/app/controllers/duodealer_app/webhooks_controller.rb +37 -0
  34. data/app/controllers/duodealer_app/webhooks_controller.rb-e +37 -0
  35. data/app/views/duodealer_app/partials/_button_styles.html.erb +104 -0
  36. data/app/views/duodealer_app/partials/_button_styles.html.erb-e +104 -0
  37. data/app/views/duodealer_app/partials/_card_styles.html.erb +33 -0
  38. data/app/views/duodealer_app/partials/_card_styles.html.erb-e +33 -0
  39. data/app/views/duodealer_app/partials/_empty_state_styles.html.erb +129 -0
  40. data/app/views/duodealer_app/partials/_empty_state_styles.html.erb-e +129 -0
  41. data/app/views/duodealer_app/partials/_layout_styles.html.erb +167 -0
  42. data/app/views/duodealer_app/partials/_layout_styles.html.erb-e +167 -0
  43. data/app/views/duodealer_app/partials/_typography_styles.html.erb +35 -0
  44. data/app/views/duodealer_app/partials/_typography_styles.html.erb-e +35 -0
  45. data/app/views/duodealer_app/sessions/enable_cookies.html.erb +75 -0
  46. data/app/views/duodealer_app/sessions/enable_cookies.html.erb-e +75 -0
  47. data/app/views/duodealer_app/sessions/new.html.erb +123 -0
  48. data/app/views/duodealer_app/sessions/new.html.erb-e +123 -0
  49. data/app/views/duodealer_app/sessions/request_storage_access.html.erb +68 -0
  50. data/app/views/duodealer_app/sessions/request_storage_access.html.erb-e +68 -0
  51. data/app/views/duodealer_app/sessions/top_level_interaction.html.erb +64 -0
  52. data/app/views/duodealer_app/sessions/top_level_interaction.html.erb-e +64 -0
  53. data/app/views/duodealer_app/shared/redirect.html.erb +23 -0
  54. data/app/views/duodealer_app/shared/redirect.html.erb-e +23 -0
  55. data/config/locales/cs.yml +23 -0
  56. data/config/locales/da.yml +20 -0
  57. data/config/locales/de.yml +22 -0
  58. data/config/locales/en.yml +15 -0
  59. data/config/locales/es.yml +22 -0
  60. data/config/locales/fi.yml +20 -0
  61. data/config/locales/fr.yml +23 -0
  62. data/config/locales/hi.yml +23 -0
  63. data/config/locales/it.yml +21 -0
  64. data/config/locales/ja.yml +17 -0
  65. data/config/locales/ko.yml +19 -0
  66. data/config/locales/ms.yml +22 -0
  67. data/config/locales/nb.yml +21 -0
  68. data/config/locales/nl.yml +21 -0
  69. data/config/locales/pl.yml +21 -0
  70. data/config/locales/pt-BR.yml +21 -0
  71. data/config/locales/pt-PT.yml +22 -0
  72. data/config/locales/sv.yml +21 -0
  73. data/config/locales/th.yml +20 -0
  74. data/config/locales/tr.yml +22 -0
  75. data/config/locales/zh-CN.yml +16 -0
  76. data/config/locales/zh-TW.yml +16 -0
  77. data/config/routes.rb +22 -0
  78. data/docs/.!20385!test-your-app.png +0 -0
  79. data/docs/.!20388!install-on-dev-shop.png +0 -0
  80. data/docs/.!62511!test-your-app.png +0 -0
  81. data/docs/.!62512!install-on-dev-shop.png +0 -0
  82. data/docs/.!62763!test-your-app.png +0 -0
  83. data/docs/.!62765!install-on-dev-shop.png +0 -0
  84. data/docs/.!63018!test-your-app.png +0 -0
  85. data/docs/.!63020!install-on-dev-shop.png +0 -0
  86. data/docs/.!63289!test-your-app.png +0 -0
  87. data/docs/.!63291!install-on-dev-shop.png +0 -0
  88. data/docs/.!63562!test-your-app.png +0 -0
  89. data/docs/.!63564!install-on-dev-shop.png +0 -0
  90. data/docs/.!63872!test-your-app.png +0 -0
  91. data/docs/.!63874!install-on-dev-shop.png +0 -0
  92. data/docs/.!64151!test-your-app.png +0 -0
  93. data/docs/.!64153!install-on-dev-shop.png +0 -0
  94. data/docs/.!64428!test-your-app.png +0 -0
  95. data/docs/.!64431!install-on-dev-shop.png +0 -0
  96. data/docs/.!64737!test-your-app.png +0 -0
  97. data/docs/.!64740!install-on-dev-shop.png +0 -0
  98. data/docs/.!65025!test-your-app.png +0 -0
  99. data/docs/.!65028!install-on-dev-shop.png +0 -0
  100. data/docs/.!65324!test-your-app.png +0 -0
  101. data/docs/.!65327!install-on-dev-shop.png +0 -0
  102. data/docs/.!65626!test-your-app.png +0 -0
  103. data/docs/.!65629!install-on-dev-shop.png +0 -0
  104. data/docs/.!65942!test-your-app.png +0 -0
  105. data/docs/.!65945!install-on-dev-shop.png +0 -0
  106. data/docs/.!66760!test-your-app.png +0 -0
  107. data/docs/.!66763!install-on-dev-shop.png +0 -0
  108. data/docs/.!67028!test-your-app.png +0 -0
  109. data/docs/.!67031!install-on-dev-shop.png +0 -0
  110. data/docs/.!67657!test-your-app.png +0 -0
  111. data/docs/.!67660!install-on-dev-shop.png +0 -0
  112. data/docs/.!68031!test-your-app.png +0 -0
  113. data/docs/.!68034!install-on-dev-shop.png +0 -0
  114. data/docs/.!68363!test-your-app.png +0 -0
  115. data/docs/.!68366!install-on-dev-shop.png +0 -0
  116. data/docs/Quickstart.md +103 -0
  117. data/docs/Releasing.md +17 -0
  118. data/docs/Troubleshooting.md +16 -0
  119. data/docs/install-on-dev-shop.png +0 -0
  120. data/docs/test-your-app.png +0 -0
  121. data/duodealer_app.gemspec +34 -0
  122. data/images/.!20334!app-proxy-screenshot.png +0 -0
  123. data/images/.!62504!app-proxy-screenshot.png +0 -0
  124. data/images/.!62754!app-proxy-screenshot.png +0 -0
  125. data/images/.!63008!app-proxy-screenshot.png +0 -0
  126. data/images/.!63277!app-proxy-screenshot.png +0 -0
  127. data/images/.!63548!app-proxy-screenshot.png +0 -0
  128. data/images/.!63855!app-proxy-screenshot.png +0 -0
  129. data/images/.!64132!app-proxy-screenshot.png +0 -0
  130. data/images/.!64407!app-proxy-screenshot.png +0 -0
  131. data/images/.!64714!app-proxy-screenshot.png +0 -0
  132. data/images/.!65000!app-proxy-screenshot.png +0 -0
  133. data/images/.!65296!app-proxy-screenshot.png +0 -0
  134. data/images/.!65594!app-proxy-screenshot.png +0 -0
  135. data/images/.!65908!app-proxy-screenshot.png +0 -0
  136. data/images/.!66724!app-proxy-screenshot.png +0 -0
  137. data/images/.!66989!app-proxy-screenshot.png +0 -0
  138. data/images/.!67614!app-proxy-screenshot.png +0 -0
  139. data/images/.!67986!app-proxy-screenshot.png +0 -0
  140. data/images/.!68314!app-proxy-screenshot.png +0 -0
  141. data/images/app-proxy-screenshot.png +0 -0
  142. data/karma.conf.js +44 -0
  143. data/lib/duodealer_app.rb +54 -0
  144. data/lib/duodealer_app/configuration.rb +85 -0
  145. data/lib/duodealer_app/controller_concerns/app_proxy_verification.rb +39 -0
  146. data/lib/duodealer_app/controller_concerns/embedded_app.rb +20 -0
  147. data/lib/duodealer_app/controller_concerns/itp.rb +44 -0
  148. data/lib/duodealer_app/controller_concerns/localization.rb +23 -0
  149. data/lib/duodealer_app/controller_concerns/login_protection.rb +180 -0
  150. data/lib/duodealer_app/controller_concerns/webhook_verification.rb +39 -0
  151. data/lib/duodealer_app/engine.rb +22 -0
  152. data/lib/duodealer_app/jobs/scripttags_manager_job.rb +17 -0
  153. data/lib/duodealer_app/jobs/webhooks_manager_job.rb +17 -0
  154. data/lib/duodealer_app/managers/scripttags_manager.rb +78 -0
  155. data/lib/duodealer_app/managers/webhooks_manager.rb +62 -0
  156. data/lib/duodealer_app/middleware/same_site_cookie_middleware.rb +69 -0
  157. data/lib/duodealer_app/session/in_memory_session_store.rb +29 -0
  158. data/lib/duodealer_app/session/session_repository.rb +33 -0
  159. data/lib/duodealer_app/session/session_storage.rb +31 -0
  160. data/lib/duodealer_app/session/storage_strategies/shop_storage_strategy.rb +25 -0
  161. data/lib/duodealer_app/session/storage_strategies/user_storage_strategy.rb +26 -0
  162. data/lib/duodealer_app/utils.rb +24 -0
  163. data/lib/duodealer_app/version.rb +5 -0
  164. data/lib/generators/duodealer_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +46 -0
  165. data/lib/generators/duodealer_app/add_after_authenticate_job/templates/after_authenticate_job.rb +10 -0
  166. data/lib/generators/duodealer_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +40 -0
  167. data/lib/generators/duodealer_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +62 -0
  168. data/lib/generators/duodealer_app/add_webhook/add_webhook_generator.rb +69 -0
  169. data/lib/generators/duodealer_app/add_webhook/templates/webhook_job.rb +8 -0
  170. data/lib/generators/duodealer_app/app_proxy_controller/app_proxy_controller_generator.rb +27 -0
  171. data/lib/generators/duodealer_app/app_proxy_controller/templates/app_proxy_controller.rb +9 -0
  172. data/lib/generators/duodealer_app/app_proxy_controller/templates/app_proxy_route.rb +10 -0
  173. data/lib/generators/duodealer_app/app_proxy_controller/templates/index.html.erb +19 -0
  174. data/lib/generators/duodealer_app/authenticated_controller/authenticated_controller_generator.rb +15 -0
  175. data/lib/generators/duodealer_app/authenticated_controller/templates/authenticated_controller.rb +5 -0
  176. data/lib/generators/duodealer_app/controllers/controllers_generator.rb +30 -0
  177. data/lib/generators/duodealer_app/duodealer_app_generator.rb +19 -0
  178. data/lib/generators/duodealer_app/home_controller/home_controller_generator.rb +27 -0
  179. data/lib/generators/duodealer_app/home_controller/templates/home_controller.rb +8 -0
  180. data/lib/generators/duodealer_app/home_controller/templates/index.html.erb +21 -0
  181. data/lib/generators/duodealer_app/install/install_generator.rb +83 -0
  182. data/lib/generators/duodealer_app/install/templates/_flash_messages.html.erb +3 -0
  183. data/lib/generators/duodealer_app/install/templates/duodealer_app.js +15 -0
  184. data/lib/generators/duodealer_app/install/templates/duodealer_app.rb +15 -0
  185. data/lib/generators/duodealer_app/install/templates/duodealer_app_index.js +2 -0
  186. data/lib/generators/duodealer_app/install/templates/duodealer_provider.rb +20 -0
  187. data/lib/generators/duodealer_app/install/templates/embedded_app.html.erb +41 -0
  188. data/lib/generators/duodealer_app/install/templates/flash_messages.js +26 -0
  189. data/lib/generators/duodealer_app/install/templates/omniauth.rb +2 -0
  190. data/lib/generators/duodealer_app/install/templates/session_store.rb +4 -0
  191. data/lib/generators/duodealer_app/install/templates/user_agent.rb +5 -0
  192. data/lib/generators/duodealer_app/rotate_duodealer_token_job/rotate_duodealer_token_job_generator.rb +16 -0
  193. data/lib/generators/duodealer_app/rotate_duodealer_token_job/templates/rotate_duodealer_token.rake +17 -0
  194. data/lib/generators/duodealer_app/rotate_duodealer_token_job/templates/rotate_duodealer_token_job.rb +42 -0
  195. data/lib/generators/duodealer_app/routes/routes_generator.rb +32 -0
  196. data/lib/generators/duodealer_app/routes/templates/routes.rb +11 -0
  197. data/lib/generators/duodealer_app/shop_model/shop_model_generator.rb +39 -0
  198. data/lib/generators/duodealer_app/shop_model/templates/db/migrate/create_shops.erb +15 -0
  199. data/lib/generators/duodealer_app/shop_model/templates/shop.rb +7 -0
  200. data/lib/generators/duodealer_app/shop_model/templates/shops.yml +3 -0
  201. data/lib/generators/duodealer_app/user_model/templates/db/migrate/create_users.erb +16 -0
  202. data/lib/generators/duodealer_app/user_model/templates/user.rb +7 -0
  203. data/lib/generators/duodealer_app/user_model/templates/users.yml +4 -0
  204. data/lib/generators/duodealer_app/user_model/user_model_generator.rb +39 -0
  205. data/lib/generators/duodealer_app/views/views_generator.rb +30 -0
  206. data/package-lock.json +7224 -0
  207. data/package.json +28 -0
  208. data/shipit.rubygems.yml +4 -0
  209. data/translation.yml +7 -0
  210. data/webpack.config.js +24 -0
  211. data/yarn.lock +5263 -0
  212. metadata +447 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ module AppProxyVerification
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ skip_before_action :verify_authenticity_token, raise: false
9
+ before_action :verify_proxy_request
10
+ end
11
+
12
+ def verify_proxy_request
13
+ return head :forbidden unless query_string_valid?(request.query_string)
14
+ end
15
+
16
+ private
17
+ def query_string_valid?(query_string)
18
+ query_hash = Rack::Utils.parse_query(query_string)
19
+
20
+ signature = query_hash.delete("signature")
21
+ return false if signature.nil?
22
+
23
+ ActiveSupport::SecurityUtils.secure_compare(
24
+ calculated_signature(query_hash),
25
+ signature
26
+ )
27
+ end
28
+
29
+ def calculated_signature(query_hash_without_signature)
30
+ sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join
31
+
32
+ OpenSSL::HMAC.hexdigest(
33
+ OpenSSL::Digest.new("sha256"),
34
+ DuodealerApp.configuration.secret,
35
+ sorted_params
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ module EmbeddedApp
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ if DuodealerApp.configuration.embedded_app?
9
+ after_action :set_esdk_headers
10
+ layout "embedded_app"
11
+ end
12
+ end
13
+
14
+ private
15
+ def set_esdk_headers
16
+ response.set_header("P3P", 'CP="Not used"')
17
+ response.headers.except!("X-Frame-Options")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ # Cookie management helpers required for ITP implementation
5
+ module Itp
6
+ private
7
+ def set_test_cookie
8
+ return unless DuodealerApp.configuration.embedded_app?
9
+ return unless user_agent_can_partition_cookies
10
+
11
+ session["duodealer.cookies_persist"] = true
12
+ end
13
+
14
+ def set_top_level_oauth_cookie
15
+ session["duodealer.top_level_oauth"] = true
16
+ end
17
+
18
+ def clear_top_level_oauth_cookie
19
+ session.delete("duodealer.top_level_oauth")
20
+ end
21
+
22
+ def user_agent_is_mobile
23
+ user_agent = BrowserSniffer.new(request.user_agent).browser_info
24
+
25
+ user_agent[:name].to_s.match(/Duo\sDealer\sMobile/)
26
+ end
27
+
28
+ def user_agent_is_pos
29
+ user_agent = BrowserSniffer.new(request.user_agent).browser_info
30
+
31
+ user_agent[:name].to_s.match(/Duo\sDealer\sPOS/)
32
+ end
33
+
34
+ def user_agent_can_partition_cookies
35
+ user_agent = BrowserSniffer.new(request.user_agent).browser_info
36
+
37
+ is_safari = user_agent[:name].to_s.match(/Safari/)
38
+
39
+ return false unless is_safari
40
+
41
+ user_agent[:version].to_s.match(/12\.0/)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ module Localization
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :set_locale
9
+ end
10
+
11
+ private
12
+ def set_locale
13
+ if params[:locale]
14
+ session[:locale] = params[:locale]
15
+ else
16
+ session[:locale] ||= I18n.default_locale
17
+ end
18
+ I18n.locale = session[:locale]
19
+ rescue I18n::InvalidLocale
20
+ I18n.locale = I18n.default_locale
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "browser_sniffer"
4
+
5
+ module DuodealerApp
6
+ module LoginProtection
7
+ extend ActiveSupport::Concern
8
+ include DuodealerApp::Itp
9
+
10
+ class DuodealerDomainNotFound < StandardError; end
11
+
12
+ included do
13
+ after_action :set_test_cookie
14
+ rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
15
+ end
16
+
17
+ def duodealer_session
18
+ return redirect_to_login unless shop_session
19
+ clear_top_level_oauth_cookie
20
+
21
+ begin
22
+ DuodealerAPI::Base.activate_session(shop_session)
23
+ yield
24
+ ensure
25
+ DuodealerAPI::Base.clear_session
26
+ end
27
+ end
28
+
29
+ def shop_session
30
+ if DuodealerApp.configuration.per_user_tokens?
31
+ return unless session[:duodealer_user]
32
+ @shop_session ||= DuodealerApp::SessionRepository.retrieve(session[:duodealer_user]["id"])
33
+ else
34
+ return unless session[:duodealer]
35
+ @shop_session ||= DuodealerApp::SessionRepository.retrieve(session[:duodealer])
36
+ end
37
+ end
38
+
39
+ def login_again_if_different_user_or_shop
40
+ if DuodealerApp.configuration.per_user_tokens?
41
+ valid_session_data = session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
42
+ sessions_do_not_match = session[:user_session] != params[:session] # current user is different from stored user
43
+
44
+ if valid_session_data && sessions_do_not_match
45
+ clear_session = true
46
+ end
47
+ end
48
+
49
+ if shop_session && params[:shop] && params[:shop].is_a?(String) && (shop_session.domain != params[:shop])
50
+ clear_session = true
51
+ end
52
+
53
+ if clear_session
54
+ clear_shop_session
55
+ redirect_to_login
56
+ end
57
+ end
58
+
59
+ protected
60
+ def redirect_to_login
61
+ if request.xhr?
62
+ head :unauthorized
63
+ else
64
+ if request.get?
65
+ path = request.path
66
+ query = sanitized_params.to_query
67
+ else
68
+ referer = URI(request.referer || "/")
69
+ path = referer.path
70
+ query = "#{referer.query}&#{sanitized_params.to_query}"
71
+ end
72
+ session[:return_to] = "#{path}?#{query}"
73
+ redirect_to(login_url_with_optional_shop)
74
+ end
75
+ end
76
+
77
+ def close_session
78
+ clear_shop_session
79
+ redirect_to(login_url_with_optional_shop)
80
+ end
81
+
82
+ def clear_shop_session
83
+ session[:duodealer] = nil
84
+ session[:duodealer_domain] = nil
85
+ session[:duodealer_user] = nil
86
+ session[:user_session] = nil
87
+ end
88
+
89
+ def login_url_with_optional_shop(top_level: false)
90
+ url = DuodealerApp.configuration.login_url
91
+
92
+ query_params = login_url_params(top_level: top_level)
93
+
94
+ url = "#{url}?#{query_params.to_query}" if query_params.present?
95
+ url
96
+ end
97
+
98
+ def login_url_params(top_level:)
99
+ query_params = {}
100
+ query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
101
+
102
+ return_to = session[:return_to] || params[:return_to]
103
+
104
+ if return_to.present? && return_to_param_required?
105
+ query_params[:return_to] = return_to
106
+ end
107
+
108
+ has_referer_shop_name = referer_sanitized_shop_name.present?
109
+
110
+ if has_referer_shop_name
111
+ query_params[:shop] ||= referer_sanitized_shop_name
112
+ end
113
+
114
+ query_params[:top_level] = true if top_level
115
+ query_params
116
+ end
117
+
118
+ def return_to_param_required?
119
+ native_params = %i[shop hmac timestamp locale protocol return_to]
120
+ request.path != "/" || sanitized_params.except(*native_params).any?
121
+ end
122
+
123
+ def fullpage_redirect_to(url)
124
+ if DuodealerApp.configuration.embedded_app?
125
+ render "duodealer_app/shared/redirect", layout: false, locals: { url: url, current_duodealer_domain: current_duodealer_domain }
126
+ else
127
+ redirect_to url
128
+ end
129
+ end
130
+
131
+ def current_duodealer_domain
132
+ duodealer_domain = sanitized_shop_name || session[:duodealer_domain]
133
+ return duodealer_domain if duodealer_domain.present?
134
+
135
+ raise DuodealerDomainNotFound
136
+ end
137
+
138
+ def sanitized_shop_name
139
+ @sanitized_shop_name ||= sanitize_shop_param(params)
140
+ end
141
+
142
+ def referer_sanitized_shop_name
143
+ return if request.referer.blank?
144
+
145
+ @referer_sanitized_shop_name ||= begin
146
+ referer_uri = URI(request.referer)
147
+ query_params = Rack::Utils.parse_query(referer_uri.query)
148
+
149
+ sanitize_shop_param(query_params.with_indifferent_access)
150
+ end
151
+ end
152
+
153
+ def sanitize_shop_param(params)
154
+ return if params[:shop].blank?
155
+ DuodealerApp::Utils.sanitize_shop_domain(params[:shop])
156
+ end
157
+
158
+ def sanitized_params
159
+ request.query_parameters.clone.tap do |query_params|
160
+ if params[:shop].is_a?(String)
161
+ query_params[:shop] = sanitize_shop_param(params)
162
+ end
163
+ end
164
+ end
165
+
166
+ def return_address
167
+ session.delete(:return_to) || DuodealerApp.configuration.root_url
168
+ end
169
+
170
+ def return_address_with_params(params)
171
+ uri = URI(return_address)
172
+ uri.query = CGI.parse(uri.query.to_s)
173
+ .symbolize_keys
174
+ .transform_values { |v| v.one? ? v.first : v }
175
+ .merge(params)
176
+ .to_query
177
+ uri.to_s
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ module WebhookVerification
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ skip_before_action :verify_authenticity_token, raise: false
9
+ before_action :verify_request
10
+ end
11
+
12
+ private
13
+ def verify_request
14
+ data = request.raw_post
15
+ return head :unauthorized unless hmac_valid?(data)
16
+ end
17
+
18
+ def hmac_valid?(data)
19
+ secrets = [DuodealerApp.configuration.secret, DuodealerApp.configuration.old_secret].reject(&:blank?)
20
+
21
+ secrets.any? do |secret|
22
+ digest = OpenSSL::Digest.new("sha256")
23
+
24
+ ActiveSupport::SecurityUtils.secure_compare(
25
+ duodealer_hmac,
26
+ Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, data))
27
+ )
28
+ end
29
+ end
30
+
31
+ def shop_domain
32
+ request.headers["HTTP_X_DUODEALER_SHOP_DOMAIN"]
33
+ end
34
+
35
+ def duodealer_hmac
36
+ request.headers["HTTP_X_DUODEALER_HMAC_SHA256"]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ class Engine < Rails::Engine
5
+ engine_name "duodealer_app"
6
+ isolate_namespace DuodealerApp
7
+
8
+ initializer "duodealer_app.assets.precompile" do |app|
9
+ app.config.assets.precompile += %w[
10
+ duodealer_app/redirect.js
11
+ duodealer_app/top_level.js
12
+ duodealer_app/enable_cookies.js
13
+ duodealer_app/request_storage_access.js
14
+ storage_access.svg
15
+ ]
16
+ end
17
+
18
+ initializer "duodealer_app.middleware" do |app|
19
+ app.config.middleware.insert_after(::Rack::Runtime, DuodealerApp::SameSiteCookieMiddleware)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ class ScripttagsManagerJob < ActiveJob::Base
5
+ queue_as do
6
+ DuodealerApp.configuration.scripttags_manager_queue_name
7
+ end
8
+
9
+ def perform(shop_domain:, shop_token:, scripttags:)
10
+ api_version = DuodealerApp.configuration.api_version
11
+ DuodealerAPI::Session.temp(domain: shop_domain, token: shop_token, api_version: api_version) do
12
+ manager = ScripttagsManager.new(scripttags, shop_domain)
13
+ manager.create_scripttags
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ class WebhooksManagerJob < ActiveJob::Base
5
+ queue_as do
6
+ DuodealerApp.configuration.webhooks_manager_queue_name
7
+ end
8
+
9
+ def perform(shop_domain:, shop_token:, webhooks:)
10
+ api_version = DuodealerApp.configuration.api_version
11
+ DuodealerAPI::Session.temp(domain: shop_domain, token: shop_token, api_version: api_version) do
12
+ manager = WebhooksManager.new(webhooks)
13
+ manager.create_webhooks
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuodealerApp
4
+ class ScripttagsManager
5
+ class CreationFailed < StandardError; end
6
+
7
+ def self.queue(shop_domain, shop_token, scripttags)
8
+ DuodealerApp::ScripttagsManagerJob.perform_later(
9
+ shop_domain: shop_domain,
10
+ shop_token: shop_token,
11
+ # Procs cannot be serialized so we interpolate now, if necessary
12
+ scripttags: build_src(scripttags, shop_domain)
13
+ )
14
+ end
15
+
16
+ def self.build_src(scripttags, domain)
17
+ scripttags.map do |tag|
18
+ next tag unless tag[:src].respond_to?(:call)
19
+ tag = tag.dup
20
+ tag[:src] = tag[:src].call(domain)
21
+ tag
22
+ end
23
+ end
24
+
25
+ attr_reader :required_scripttags, :shop_domain
26
+
27
+ def initialize(scripttags, shop_domain)
28
+ @required_scripttags = scripttags
29
+ @shop_domain = shop_domain
30
+ end
31
+
32
+ def recreate_scripttags!
33
+ destroy_scripttags
34
+ create_scripttags
35
+ end
36
+
37
+ def create_scripttags
38
+ return if required_scripttags.blank?
39
+
40
+ expanded_scripttags.each do |scripttag|
41
+ create_scripttag(scripttag) unless scripttag_exists?(scripttag[:src])
42
+ end
43
+ end
44
+
45
+ def destroy_scripttags
46
+ scripttags = expanded_scripttags
47
+ DuodealerAPI::ScriptTag.all.each do |tag|
48
+ DuodealerAPI::ScriptTag.delete(tag.id) if is_required_scripttag?(scripttags, tag)
49
+ end
50
+
51
+ @current_scripttags = nil
52
+ end
53
+
54
+ private
55
+ def expanded_scripttags
56
+ self.class.build_src(required_scripttags, shop_domain)
57
+ end
58
+
59
+ def is_required_scripttag?(scripttags, tag)
60
+ scripttags.map { |w| w[:src] }.include? tag.src
61
+ end
62
+
63
+ def create_scripttag(attributes)
64
+ attributes.reverse_merge!(format: "json")
65
+ scripttag = DuodealerAPI::ScriptTag.create(attributes)
66
+ raise CreationFailed, scripttag.errors.full_messages.to_sentence unless scripttag.persisted?
67
+ scripttag
68
+ end
69
+
70
+ def scripttag_exists?(src)
71
+ current_scripttags[src]
72
+ end
73
+
74
+ def current_scripttags
75
+ @current_scripttags ||= DuodealerAPI::ScriptTag.all.index_by(&:src)
76
+ end
77
+ end
78
+ end