whiplash-app 0.9.2 → 0.9.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90618c9787cb8660f4fcb0be146bdb14736ce8635274f9b262f68813268bd35d
4
- data.tar.gz: a26cca38e01f5c5642b94afe39cbc0be6ea08f05c9ccf446d0d802e5548aa100
3
+ metadata.gz: e0e2ae334bdd40de1ae96543cd86eb7506e3de9a7d65d04b003fa67d2eb83da1
4
+ data.tar.gz: a2ae0fe3f75c4ed25e7703c85f71495609f10434c9bf3d687483d1ad440a5513
5
5
  SHA512:
6
- metadata.gz: d67a3f4a633319407cb8dd9a6717a92b57480825dab3fa36b4e77fda280f58c16089aa833a39fade900dee77efeaa169b868e3ba5f0e25832e37105da40ad53b
7
- data.tar.gz: 94df31ed65be70e565fb5ea8ba2744dbfbbfe386500d8ae5c57bce4bd72474ff0af70f4ea9620e02f2a108282d24821c6477b0107d0b55146249c690010f8d07
6
+ metadata.gz: 1fd9e63cc8d1359a38adc462eb4818928d4ea84bc1e57db46e1fa9725ef547c59172763727f8fc7957c8de22d0a3720a437040789966116ce5a1c0a833ad210f
7
+ data.tar.gz: 0f35b12647a50d4a4c8fbb1a0f24fb6d40946c19dbbeb4d0238eb19d5c075fd3958e2cea68a72072d8db5770c83d18a5c1bc831a3bee2497e9cc7a4bfde52913
data/README.md CHANGED
@@ -4,6 +4,8 @@ The whiplash-app gem allows your Whiplash application to access the Whiplash
4
4
  API and perform authentication, signatures and signature verification, and basic
5
5
  CRUD functions against the api.
6
6
 
7
+ For apps that provide a UI, it also provides built in authentication and several helper methods.
8
+
7
9
  ## Installation
8
10
 
9
11
  Add this line to your application's Gemfile:
@@ -22,109 +24,68 @@ Or install it yourself as:
22
24
 
23
25
  ## Usage
24
26
 
25
- **NOTE: 0.4.0 introduces a breaking change and is NOT backward compatible with previous versions.**
26
-
27
- To upgrade from < 0.4.0, you need to make two small changes:
28
- 1. `Whiplash::App` must now be instantiated.
29
- 2. Tokens are **not** automatically refreshed
27
+ There are two basic uses for this gem:
28
+ 1. Authenticating users for apps _with a UI_ (i.e. Notifications, Troubleshoot, etc)
29
+ 2. Providing offline access to applications that perform tasks (i.e Tasks, Old Integrations, etc)
30
30
 
31
- Before:
32
- ```ruby
33
- api = Whiplash::App
34
- ```
31
+ It's not uncommon for an application to do _both_ of the above (i.e. Notifications, Payments, etc)
35
32
 
36
- After:
37
- ```ruby
38
- api = Whiplash::App.new
39
- api.refresh_token! # Since you don't have one yet
40
- api.token # Confirm you've got a token
41
- . . .
42
- api.refresh_token! if api.token_expired?
43
- ```
44
-
45
- ### Authentication
33
+ ### Authentication for offline access (Oauth Client Credentials flow)
46
34
  In order to authenticate, make sure the following `ENV` vars are set:
47
35
 
48
36
  ```ruby
49
- ENV["WHIPLASH_CLIENT_ID"]
50
- ENV["WHIPLASH_CLIENT_SECRET"]
51
- ENV["WHIPLASH_CLIENT_SCOPE"]
37
+ ENV['WHIPLASH_API_URL']
38
+ ENV['WHIPLASH_CLIENT_ID']
39
+ ENV['WHIPLASH_CLIENT_SCOPE']
40
+ ENV['WHIPLASH_CLIENT_SECRET']
52
41
  ```
53
42
 
54
- Once those are set, authentication is handled in app.
55
-
56
- ### Oauth Client Credentials
57
- You can authenticate using Oauth Client Credentials (i.e. auth an entire app).
58
- You probably want this for apps that work offline, _on behalf_ of users or customers, or that don't work at the user/customer-level at all.
43
+ Once those are set, you can generate and use an access token like so:
59
44
 
60
45
  ```ruby
61
- api = Whiplash::App.new
62
- api.refresh_token! # Since you don't have one yet
63
- api.token # Confirm you've got a token
46
+ token = Whiplash::App.client_credentials_token
47
+ api = Whiplash::App.new(token)
48
+ customers = api.get!('customers')
64
49
  ```
65
50
 
66
- ### Oauth Authorization Code
67
- You can also authenticate using Oauth Authorization Code (i.e. auth an individual user). This is most common for user-facing app's with a front end.
51
+ ### Authentication for online access
52
+ In order to use the API, you only need to set the following:
68
53
 
69
54
  ```ruby
70
- # Authenticate using Devise Omniauthenticateable strategy; you'll get oauth creds back as a hash
71
- api = Whiplash::App.new(oauth_credentials_hash)
72
- api.token # Confirm you've got a token
55
+ ENV['WHIPLASH_API_URL']
73
56
  ```
74
57
 
75
- ### API URL
76
- In order to set your api url, you can use the following environment URL:
77
- ```
78
- ENV["WHIPLASH_API_URL"]
79
- ```
80
- If it isn't set, then the API URL defaults to either `https://sandbox.getwhiplash.com` (test or dev environment) or `https://www.getwhiplash.com` (prod environment).
58
+ As long as all of your apps are on the same subdomain, they will share auth cookies:
81
59
 
82
- ### Sending Customer ID and Shop ID headers
83
- You can send the headers in `headers` array, like `{customer_id: 123, shop_id: 111}`.
84
- Alternatively, you can set them on instantiation like `Whiplash::App.new(token, {customer_id: 123, shop_id: 111})`.
85
-
86
- ### Rails AR type calls
87
-
88
- In order to make the use of the gem seem more "AR-ish", we've added AR oriented methods that can be used for basic object creation/deletion/updating/viewing. The basic gist of these AR style CRUD methods is that they will all follow the same pattern. If you are performing a collection action, such as `create` or `find`, the pattern is this:
89
-
90
- ```ruby
91
- api.create(resource, params, headers)
60
+ ```json
61
+ {
62
+ "oauth_token": XXXXXXX,
63
+ "user": {"id":151,"email":"mark@getwhiplash.com","role":"admin","locale":"en","first_name":"Mark","last_name":"Dickson","partner_id":null,"warehouse_id": 1,"customer_ids":[1, 2, 3]}
64
+ }
92
65
  ```
93
66
 
94
- For member actions, such as `show`, or `destroy` methods, the pattern is this:
67
+ You get a variety of helper methods for free:
95
68
 
96
- ```ruby
97
- api.find(resource, id, headers)
98
- api.destroy(resource, id, headers)
99
- ```
100
-
101
- Finally, for `update` calls, it's a mixture of those:
69
+ `init_whiplash_api` - This instantiates `@whiplash_api` which can be used to make requests, out of the box
70
+ `current_user` - This is a **hash** with the above fields; you typically shouldn't need much more user info than this
71
+ `require_user` - Typically you'd use this in a `before_action`. You almost always want this in `ApplicationController`.
72
+ `set_locale!` - Sets the locale based on the value in the user hash
73
+ `set_current_user_cookie!` - Updates the current user cookie with fresh data from the api. You typically won't need this, unless your app updates fields like `warehouse_id` or `locale`.
74
+ `core_url` - Shorthand for `ENV['WHIPLASH_API_URL']`
75
+ `core_url_for` - Link back to Core like `core_url_for('login')`
102
76
 
103
- ```ruby
104
- api.update(resource, id, params_to_update, headers)
105
- ```
106
77
 
107
- So, basic AR style calls can be performed like so:
78
+ ### Sending Customer ID and Shop ID headers
79
+ You can send the headers in `headers` array, like `{customer_id: 123, shop_id: 111}`.
80
+ Alternatively, you can set them on instantiation like `Whiplash::App.new(token, {customer_id: 123, shop_id: 111})`
108
81
 
109
- ```ruby
110
- api.find_all('orders', {}, { customer_id: 187 })
111
- api.find('orders', 1)
112
- api.create('orders', { key: "value", key2: "value" }, { customer_id: 187 } )
113
- api.update('orders', 1, { key: "value"}, { customer_id: 187 } )
114
- api.destroy('orders', 1, { customer_id: 187 } )
115
- api.count('customers')
116
- ```
117
82
 
118
83
  ### CRUD Wrapper methods
119
- In reality, all of these methods are simply wrapper methods around simple `GET/POST/PUT/DELETE` wrappers on Faraday, so if you want to get more granular,you can also make calls that simply reference the lower level REST verb:
120
84
 
121
85
  ```ruby
122
86
  api.get('orders')
123
87
  ```
124
- Which will return all orders and roughly correspond to an index call. If you need to use `Whiplash::App` for nonRESTful calls, simply drop the full endpoint in as your first argument:
125
88
 
126
- ```ruby
127
- api.get('orders/non_restful_action', {}, {})
128
89
  ```
129
90
  `POST`, `PUT`, and `DELETE` calls can be performed in much the same way:
130
91
  ```ruby
@@ -133,6 +94,37 @@ api.put(endpoint, params, headers) # PUT request to the specified endpoint passi
133
94
  api.delete(endpoint, params, headers) # DELETE request to the specified endpoint. Params would probably just be an id.
134
95
  ```
135
96
 
97
+ ### Bang methods
98
+
99
+ In typical Rails/Ruby fashion, `!` methods `raise`. Typically, you'll want to set some global `rescue`s and use the `!` version of crud requests:
100
+
101
+ ```ruby
102
+ rescue_from WhiplashApiError, with: :handle_whiplash_api_error
103
+
104
+ def handle_whiplash_api_error(exception)
105
+ # Any special exceptions we want to handle directly
106
+ case exception.class.to_s
107
+ when 'WhiplashApiError::Unauthorized'
108
+ return redirect_to core_url_for('logout')
109
+ end
110
+
111
+ @status_code = WhiplashApiError.codes&.invert&.dig(exception&.class)
112
+ @error = exception.message
113
+ respond_to do |format|
114
+ format.html {
115
+ flash[:error] = @error
116
+ redirect_back(fallback_location: root_path)
117
+ }
118
+ format.json {
119
+ render json: exception, status: @status_code
120
+ }
121
+ format.js {
122
+ render template: 'resources/exception'
123
+ }
124
+ end
125
+ end
126
+ ```
127
+
136
128
  ### Signing and Verifying.
137
129
  `whiplash-app` supports signing and verifying signatures like so:
138
130
  ```ruby
@@ -143,30 +135,6 @@ and verifications are done like so:
143
135
  Whiplash::App.verified?(request)
144
136
  ```
145
137
 
146
- ### Caching
147
- `whiplash-app` is Cache agnostic, relying on the `moneta` gem to provide a local store, if needed.
148
- However, if you intend to specify `REDIS` as your key-value store of choice, it's dead simple. Simply declare the following variables:
149
- ```
150
- ENV["REDIS_HOST"]
151
- ENV["REDIS_PORT"]
152
- ENV["REDIS_PASSWORD"]
153
- ENV["REDIS_NAMESPACE"]
154
- ```
155
- If those are provided, `moneta` will use your redis connection and will namespace your cache storage under the redis namespace. By default, if you do not declare a `REDIS_NAMESPACE` value, the app will default to the `WHIPLASH_CLIENT_ID`.
156
-
157
- **For user-facing apps, best practice is to store the `oauth_credentials_hash` in a session variable.**
158
-
159
- ### Gotchas
160
- Due to the way Faraday handles params, this would not, as expected, route to `orders#show` in the Whiplash App, but would instead route to `orders#index`, so it wouldn't return the expected singular order with an ID of 1, but all orders for that customer.
161
- ```ruby
162
- api.get('orders', {id: 1}, {customer_id: 187})
163
- ```
164
- Instead, you'd want to do:
165
- ```ruby
166
- api.get('orders/1', {}, {customer_id: 187})
167
- ```
168
-
169
-
170
138
  ## Development
171
139
 
172
140
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -10,6 +10,12 @@ module Whiplash
10
10
  end
11
11
  end
12
12
 
13
+ def rate_limit
14
+ (ENV['WHIPLASH_RATE_LIMIT'] || 25).to_i
15
+ end
16
+
17
+ private
18
+
13
19
  def production_url
14
20
  ENV["WHIPLASH_API_URL"] || "https://www.getwhiplash.com"
15
21
  end
@@ -18,10 +24,6 @@ module Whiplash
18
24
  ENV["WHIPLASH_API_URL"] || "https://sandbox.getwhiplash.com"
19
25
  end
20
26
 
21
- def rate_limit
22
- (ENV['WHIPLASH_RATE_LIMIT'] || 25).to_i
23
- end
24
-
25
27
  end
26
28
  end
27
29
  end
@@ -27,7 +27,7 @@ module Whiplash
27
27
 
28
28
  def app_request(options={})
29
29
  return base_app_request(options) unless defined?(Sidekiq)
30
- limiter = Sidekiq::Limiter.window('whiplash-core', self.rate_limit, :second, wait_timeout: 15)
30
+ limiter = Sidekiq::Limiter.window('whiplash-core', self.class.rate_limit, :second, wait_timeout: 15)
31
31
  limiter.within_limit do
32
32
  base_app_request(options)
33
33
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+ module Whiplash
3
+ class App
4
+ module ControllerHelpers
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ helper_method :cookie_domain,
9
+ :core_url,
10
+ :core_url_for,
11
+ :current_user
12
+ end
13
+
14
+ private
15
+
16
+ def cookie_domain
17
+ '.' + URI.parse(core_url).host
18
+ end
19
+
20
+ def core_url
21
+ ENV['WHIPLASH_API_URL']
22
+ end
23
+
24
+ def core_url_for(path)
25
+ [core_url, path].join('/')
26
+ end
27
+
28
+ def current_user
29
+ return if cookies[:user].blank?
30
+ begin
31
+ @current_user ||= JSON.parse(cookies[:user])
32
+ rescue StandardError => e
33
+ Rails.logger.warn "User could not be initialized: #{e.message}"
34
+ @current_user = nil
35
+ end
36
+ end
37
+
38
+ def http_scheme
39
+ URI(core_url).scheme
40
+ end
41
+
42
+ def init_whiplash_api(options = {})
43
+ return redirect_to core_url_for('login') if cookies[:oauth_token].blank?
44
+ token = {access_token: cookies[:oauth_token]}
45
+ begin
46
+ @whiplash_api = Whiplash::App.new(token, options)
47
+ rescue StandardError => e
48
+ Rails.logger.warn "API failed to initialize: #{e.message}"
49
+ @whiplash_api = nil
50
+ end
51
+ end
52
+
53
+ def require_user
54
+ redirect_to core_url_for('login') if current_user.blank?
55
+ end
56
+
57
+ def set_locale!
58
+ I18n.default_locale = :en
59
+ I18n.locale = current_user.try('locale') || I18n.default_locale
60
+ end
61
+
62
+
63
+ def set_current_user_cookie!(expires_at = nil)
64
+ user = @whiplash_api.get!("me").body
65
+ fields_we_care_about = %w(id email role locale first_name last_name partner_id warehouse_id customer_ids)
66
+ user_hash = user.slice(*fields_we_care_about)
67
+ expires_at ||= user['current_sign_in_expires_at']
68
+
69
+ shared_values = {
70
+ expires: DateTime.parse(expires_at),
71
+ secure: http_scheme == 'https',
72
+ samesite: :strict,
73
+ domain: cookie_domain
74
+ }
75
+ cookies[:user] = shared_values.merge(value: user_hash.to_json)
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -1,14 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whiplash
4
- module Logs
4
+ class App
5
5
  class Railtie < Rails::Railtie
6
+
7
+ config.before_configuration do |app|
8
+ # App name/etc, mainly for consistency in logging
9
+ app_name = app.class.module_parent.name.underscore.dasherize
10
+ app.config.environment_key = ENV.fetch('ENVIRONMENT_KEY', Rails.env.to_s)
11
+ app.config.application_key = ENV.fetch('APPLICATION_KEY', app_name)
12
+ app.config.application_name_space = [config.application_key, config.environment_key].join('-')
13
+
14
+ # session settings
15
+ session_days = 30
16
+ session_seconds = session_days * 24 * 60 * 60
17
+ session_length = ENV.fetch('SESSION_LENGTH', session_seconds).to_i
18
+ app.config.session_length = session_length
19
+ app.config.session_store :cookie_store, :key => '_session', :expire_after => session_length
20
+ end
21
+
6
22
  initializer "whiplash_app.action_controller" do
7
23
  ActiveSupport.on_load(:action_controller) do
8
- puts "Extending #{self} with YourGemsModuleName::Controller"
9
24
  include Whiplash::App::CanonicalHost
25
+ include Whiplash::App::ControllerHelpers
10
26
  end
11
27
  end
28
+
12
29
  end
13
30
  end
14
31
  end
@@ -1,5 +1,5 @@
1
1
  module Whiplash
2
2
  class App
3
- VERSION = "0.9.2"
3
+ VERSION = "0.9.5"
4
4
  end
5
5
  end
data/lib/whiplash/app.rb CHANGED
@@ -9,16 +9,17 @@ require "faraday"
9
9
 
10
10
  # Rails app stuff
11
11
  if defined?(Rails::Railtie)
12
- require "whiplash/app/canonical_host"
13
12
  require "whiplash/app/railtie"
13
+ require "whiplash/app/canonical_host"
14
+ require "whiplash/app/controller_helpers"
14
15
  end
15
16
 
16
17
  module Whiplash
17
18
  class App
18
- include Whiplash::App::ApiConfig
19
+ extend Whiplash::App::Signing
20
+ extend Whiplash::App::ApiConfig
19
21
  include Whiplash::App::Connections
20
22
  include Whiplash::App::FinderMethods
21
- extend Whiplash::App::Signing
22
23
 
23
24
  attr_accessor :customer_id, :shop_id, :token
24
25
 
@@ -29,16 +30,16 @@ module Whiplash
29
30
  @api_version = options[:api_version] || 2 # can be 2_1
30
31
  end
31
32
 
32
- def client
33
- OAuth2::Client.new(ENV["WHIPLASH_CLIENT_ID"], ENV["WHIPLASH_CLIENT_SECRET"], site: api_url)
34
- end
35
-
36
33
  def versioned_api_url
37
34
  "api/v#{@api_version}"
38
35
  end
39
36
 
37
+ def client
38
+ OAuth2::Client.new(ENV["WHIPLASH_CLIENT_ID"], ENV["WHIPLASH_CLIENT_SECRET"], site: self.class.api_url)
39
+ end
40
+
40
41
  def connection
41
- Faraday.new [api_url, versioned_api_url].join("/") do |conn|
42
+ Faraday.new [self.class.api_url, versioned_api_url].join("/") do |conn|
42
43
  conn.request :authorization, 'Bearer', token.token
43
44
  conn.request :json
44
45
  conn.response :json, :content_type => /\bjson$/
@@ -53,9 +54,9 @@ module Whiplash
53
54
  case ENV["WHIPLASH_CLIENT_SCOPE"]
54
55
  when /app_(manage|read)/
55
56
  begin
56
- access_token = client.client_credentials.get_token(scope: ENV["WHIPLASH_CLIENT_SCOPE"])
57
+ access_token = self.class.client_credentials_token
57
58
  rescue URI::InvalidURIError => e
58
- raise StandardError, "The provide URL (#{ENV["WHIPLASH_API_URL"]}) is not valid"
59
+ raise StandardError, "The provided URL (#{ENV["WHIPLASH_API_URL"]}) is not valid"
59
60
  end
60
61
  else
61
62
  raise StandardError, "You must request an access token before you can refresh it" if token.nil?
@@ -70,6 +71,13 @@ module Whiplash
70
71
  false
71
72
  end
72
73
 
74
+ class << self
75
+ def client_credentials_token
76
+ client = OAuth2::Client.new(ENV["WHIPLASH_CLIENT_ID"], ENV["WHIPLASH_CLIENT_SECRET"], site: api_url)
77
+ client.client_credentials.get_token(scope: ENV["WHIPLASH_CLIENT_SCOPE"])
78
+ end
79
+ end
80
+
73
81
  private
74
82
  def format_token(oauth_token)
75
83
  return oauth_token if oauth_token.is_a?(OAuth2::AccessToken)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whiplash-app
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Don Sullivan, Mark Dickson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-20 00:00:00.000000000 Z
11
+ date: 2024-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oauth2
@@ -117,6 +117,7 @@ files:
117
117
  - lib/whiplash/app/api_config.rb
118
118
  - lib/whiplash/app/canonical_host.rb
119
119
  - lib/whiplash/app/connections.rb
120
+ - lib/whiplash/app/controller_helpers.rb
120
121
  - lib/whiplash/app/finder_methods.rb
121
122
  - lib/whiplash/app/railtie.rb
122
123
  - lib/whiplash/app/signing.rb