fulfil_api 0.7.2 → 0.8.1
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 +4 -4
- data/.rubocop.yml +9 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +20 -0
- data/README.md +149 -2
- data/app/controllers/concerns/fulfil_api/authenticated.rb +43 -0
- data/app/controllers/concerns/fulfil_api/installation_scoped.rb +56 -0
- data/app/controllers/fulfil_api/application_controller.rb +31 -0
- data/app/controllers/fulfil_api/installations_controller.rb +111 -0
- data/app/models/concerns/fulfil_api/installable.rb +47 -0
- data/app/models/fulfil_api/installation.rb +145 -0
- data/config/routes.rb +9 -0
- data/lib/fulfil_api/circuit_open.rb +9 -0
- data/lib/fulfil_api/client.rb +10 -6
- data/lib/fulfil_api/configuration.rb +72 -8
- data/lib/fulfil_api/engine.rb +14 -0
- data/lib/fulfil_api/middleware/circuit_breaker.rb +44 -0
- data/lib/fulfil_api/middleware.rb +59 -0
- data/lib/fulfil_api/oauth/authorization.rb +181 -0
- data/lib/fulfil_api/oauth/configuration.rb +96 -0
- data/lib/fulfil_api/oauth/token.rb +78 -0
- data/lib/fulfil_api/oauth.rb +37 -0
- data/lib/fulfil_api/relation.rb +2 -2
- data/lib/fulfil_api/resource/attribute_assignable.rb +12 -12
- data/lib/fulfil_api/tpl_client.rb +10 -6
- data/lib/fulfil_api/version.rb +1 -1
- data/lib/fulfil_api.rb +11 -0
- data/lib/generators/fulfil_api/install/install_generator.rb +72 -0
- data/lib/generators/fulfil_api/install/templates/create_fulfil_api_installations.rb +43 -0
- data/lib/generators/fulfil_api/install/templates/fulfil_api.rb +38 -0
- metadata +48 -11
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FulfilApi
|
|
4
|
+
module OAuth
|
|
5
|
+
# Holds the credentials and options of the OAuth app that was registered in
|
|
6
|
+
# Fulfil's authentication dashboard, plus the Rails-specific settings used
|
|
7
|
+
# by {FulfilApi::Engine}.
|
|
8
|
+
#
|
|
9
|
+
# @see https://auth.fulfil.io/user/clients
|
|
10
|
+
class Configuration
|
|
11
|
+
ACCESS_TYPES = %i[offline_access user_session].freeze
|
|
12
|
+
|
|
13
|
+
# Fulfil's own default is `user_session`, but the gem defaults to
|
|
14
|
+
# `offline_access`. An app that talks to Fulfil from background jobs
|
|
15
|
+
# needs a token that outlives the user's web session, and personal
|
|
16
|
+
# access tokens are no longer a viable alternative now that they expire.
|
|
17
|
+
DEFAULT_ACCESS_TYPE = :offline_access
|
|
18
|
+
DEFAULT_AFTER_INSTALL_PATH = "/"
|
|
19
|
+
DEFAULT_MOUNT_PATH = "/fulfil"
|
|
20
|
+
DEFAULT_SCOPES = [].freeze
|
|
21
|
+
|
|
22
|
+
# What the engine's controllers inherit from until {#parent_controller} is
|
|
23
|
+
# set. It carries no authentication, which is why the flow refuses to run
|
|
24
|
+
# before an application has named a controller of its own.
|
|
25
|
+
UNCONFIGURED_PARENT_CONTROLLER = "ActionController::Base"
|
|
26
|
+
|
|
27
|
+
attr_accessor :after_install_path, :client_id, :client_secret, :parent_controller, :redirect_uri
|
|
28
|
+
attr_reader :access_type, :scopes
|
|
29
|
+
|
|
30
|
+
# @param options [Hash, nil] An optional list of configuration options.
|
|
31
|
+
def initialize(options = {})
|
|
32
|
+
(options || {}).each_pair do |key, value|
|
|
33
|
+
public_send(:"#{key}=", value) if respond_to?(:"#{key}=")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
set_default_options
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param value [Symbol, String] Either `:offline_access` or `:user_session`.
|
|
40
|
+
# @raise [ArgumentError] When the access type is not supported by Fulfil.
|
|
41
|
+
# @return [void]
|
|
42
|
+
def access_type=(value)
|
|
43
|
+
@access_type = value.to_sym
|
|
44
|
+
|
|
45
|
+
return if ACCESS_TYPES.include?(@access_type)
|
|
46
|
+
|
|
47
|
+
raise ArgumentError, "#{value} is not a valid access type. Use :offline_access or :user_session instead."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [true, false] Whether an access token can be requested at all.
|
|
51
|
+
def configured?
|
|
52
|
+
client_id.present? && client_secret.present?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [true, false] Whether a permanent, user-independent token is requested.
|
|
56
|
+
def offline_access?
|
|
57
|
+
access_type == :offline_access
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The class the engine's controllers inherit from.
|
|
61
|
+
#
|
|
62
|
+
# @return [Class]
|
|
63
|
+
def parent_controller_class
|
|
64
|
+
(parent_controller.presence || UNCONFIGURED_PARENT_CONTROLLER).constantize
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Whether the application named the controller the OAuth flow runs under.
|
|
68
|
+
#
|
|
69
|
+
# There is no sensible default: the flow has to run behind whatever the
|
|
70
|
+
# application uses to authenticate a request, and guessing wrong leaves
|
|
71
|
+
# an endpoint that lets a stranger install their own Fulfil workspace
|
|
72
|
+
# over the application's.
|
|
73
|
+
#
|
|
74
|
+
# @return [true, false]
|
|
75
|
+
def parent_controller_configured?
|
|
76
|
+
parent_controller.present?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param value [Array<String>, String, nil] A list of scopes, or a string
|
|
80
|
+
# of scopes separated by commas or spaces.
|
|
81
|
+
# @return [void]
|
|
82
|
+
def scopes=(value)
|
|
83
|
+
@scopes = Array(value).flat_map { |scope| scope.to_s.split(Token::SCOPE_SEPARATOR) }.reject(&:empty?).uniq
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# @return [void]
|
|
89
|
+
def set_default_options
|
|
90
|
+
self.access_type = DEFAULT_ACCESS_TYPE if access_type.nil?
|
|
91
|
+
self.after_install_path = DEFAULT_AFTER_INSTALL_PATH if after_install_path.nil?
|
|
92
|
+
self.scopes = DEFAULT_SCOPES if scopes.nil?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FulfilApi
|
|
4
|
+
module OAuth
|
|
5
|
+
# Wraps the payload returned by Fulfil's `POST /oauth/token` endpoint.
|
|
6
|
+
#
|
|
7
|
+
# @example An `offline_access` payload
|
|
8
|
+
# {
|
|
9
|
+
# "access_token" => "user-xxxxx-xxxxxxx",
|
|
10
|
+
# "associated_user" => { "email" => "first.last@example.com", "id" => 1, "name" => "First Last" },
|
|
11
|
+
# "expires_in" => 3600,
|
|
12
|
+
# "offline_access_token" => "bot-xxxx",
|
|
13
|
+
# "scope" => ["user_session,party.party"],
|
|
14
|
+
# "token_type" => "Bearer"
|
|
15
|
+
# }
|
|
16
|
+
class Token
|
|
17
|
+
DEFAULT_TOKEN_TYPE = "Bearer"
|
|
18
|
+
SCOPE_SEPARATOR = /[\s,]+/
|
|
19
|
+
|
|
20
|
+
attr_reader :access_token, :associated_user, :expires_in, :offline_access_token, :scopes, :token_type
|
|
21
|
+
|
|
22
|
+
# @param payload [Hash] The parsed response body of the token endpoint.
|
|
23
|
+
def initialize(payload)
|
|
24
|
+
payload = (payload || {}).with_indifferent_access
|
|
25
|
+
|
|
26
|
+
@access_token = payload[:access_token].presence
|
|
27
|
+
@associated_user = (payload[:associated_user] || {}).with_indifferent_access
|
|
28
|
+
@expires_in = payload[:expires_in]&.to_i
|
|
29
|
+
@offline_access_token = payload[:offline_access_token].presence
|
|
30
|
+
@scopes = extract_scopes(payload[:scope])
|
|
31
|
+
@token_type = payload[:token_type].presence || DEFAULT_TOKEN_TYPE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The moment the short-lived {#access_token} stops working. The
|
|
35
|
+
# {#offline_access_token} is not covered by this: it stays valid until
|
|
36
|
+
# the app is uninstalled from the workspace.
|
|
37
|
+
#
|
|
38
|
+
# @return [Time, nil]
|
|
39
|
+
def expires_at
|
|
40
|
+
return if expires_in.nil?
|
|
41
|
+
|
|
42
|
+
Time.now.utc + expires_in
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [true, false] Whether a permanent token was granted.
|
|
46
|
+
def offline?
|
|
47
|
+
offline_access_token.present?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Prefers the permanent token over the short-lived one, so a token used
|
|
51
|
+
# outside of the request it was granted in keeps working.
|
|
52
|
+
#
|
|
53
|
+
# @return [FulfilApi::AccessToken]
|
|
54
|
+
def to_access_token
|
|
55
|
+
FulfilApi::AccessToken.new(value, type: :oauth)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [String, nil] The token to authenticate API requests with.
|
|
59
|
+
def value
|
|
60
|
+
offline_access_token || access_token
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Fulfil returns the granted scopes as an array holding a single string
|
|
66
|
+
# rather than as a list of scopes. Its docs show that string comma
|
|
67
|
+
# separated (`["user_session,party.party"]`), but a real workspace
|
|
68
|
+
# separates them with spaces (`["sale.order stock.move.view"]`), so both
|
|
69
|
+
# are flattened into a plain list.
|
|
70
|
+
#
|
|
71
|
+
# @param scope [Array<String>, String, nil] The granted scopes.
|
|
72
|
+
# @return [Array<String>]
|
|
73
|
+
def extract_scopes(scope)
|
|
74
|
+
Array(scope).flat_map { |value| value.to_s.split(SCOPE_SEPARATOR) }.reject(&:empty?).uniq
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FulfilApi
|
|
4
|
+
# The {FulfilApi::OAuth} namespace contains everything needed to obtain an
|
|
5
|
+
# access token through Fulfil's OAuth 2.0 authorization flow.
|
|
6
|
+
#
|
|
7
|
+
# Fulfil's flow has two access modes. The `user_session` mode returns a
|
|
8
|
+
# short-lived token tied to the user's web session, while the
|
|
9
|
+
# `offline_access` mode additionally returns an `offline_access_token` that
|
|
10
|
+
# remains valid until the app is uninstalled. There is no refresh token in
|
|
11
|
+
# either mode: an expired `user_session` token can only be replaced by
|
|
12
|
+
# sending the user through the flow again.
|
|
13
|
+
#
|
|
14
|
+
# @see https://docs.fulfil.io/developers/rest-api/authentication/oauth
|
|
15
|
+
module OAuth
|
|
16
|
+
class Error < FulfilApi::Error; end
|
|
17
|
+
|
|
18
|
+
# Raised when the flow is started without a client ID or secret.
|
|
19
|
+
class ConfigurationError < Error; end
|
|
20
|
+
|
|
21
|
+
# Raised when the merchant's workspace ID is missing or malformed. The
|
|
22
|
+
# workspace ID ends up in the host of the URL the user is redirected to,
|
|
23
|
+
# so an unchecked value would allow redirecting to an arbitrary host.
|
|
24
|
+
class MerchantIdInvalid < Error; end
|
|
25
|
+
|
|
26
|
+
# Raised when the user cancelled the installation, or when Fulfil reported
|
|
27
|
+
# an error on the callback.
|
|
28
|
+
class AuthorizationDenied < Error; end
|
|
29
|
+
|
|
30
|
+
# Raised when the `state` on the callback does not match the one generated
|
|
31
|
+
# when the flow was started, which means the callback cannot be trusted.
|
|
32
|
+
class StateMismatch < Error; end
|
|
33
|
+
|
|
34
|
+
# Raised when exchanging the authorization code for an access token failed.
|
|
35
|
+
class TokenExchangeFailed < Error; end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/fulfil_api/relation.rb
CHANGED
|
@@ -48,8 +48,8 @@ module FulfilApi
|
|
|
48
48
|
#
|
|
49
49
|
# @yield [resource] Yields each resource object to the given block.
|
|
50
50
|
# @return [Enumerator, self] Returns an Enumerator if no block is given; otherwise, returns self.
|
|
51
|
-
def each(&
|
|
52
|
-
all.each(&
|
|
51
|
+
def each(&)
|
|
52
|
+
all.each(&)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Resets any of the previously provided query conditions.
|
|
@@ -76,23 +76,23 @@ module FulfilApi
|
|
|
76
76
|
# $ build_attribute("warehouse.id", 10)
|
|
77
77
|
# => { "warehouse" => { "id" => 10 } }
|
|
78
78
|
#
|
|
79
|
-
# @
|
|
79
|
+
# @example attribute where the relation and its field share a name
|
|
80
|
+
# $ build_attribute("tracking_number.tracking_number", "T04DDA5014242536")
|
|
81
|
+
# => { "tracking_number" => { "tracking_number" => "T04DDA5014242536" } }
|
|
82
|
+
#
|
|
83
|
+
# @param name [String, Symbol] The attribute name
|
|
80
84
|
# @param value [Any] The attribute value
|
|
81
85
|
# @return [Hash] The newly build attribute
|
|
82
|
-
def build_attribute(name, value)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
attribute_level = attribute
|
|
86
|
+
def build_attribute(name, value)
|
|
87
|
+
*relation_names, attribute_name = name.to_s.split(".")
|
|
88
|
+
return {} if attribute_name.nil?
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else
|
|
91
|
-
attribute_level[attribute_name] ||= {}
|
|
92
|
-
attribute_level = attribute_level[attribute_name]
|
|
93
|
-
end
|
|
90
|
+
attribute = {}
|
|
91
|
+
attribute_level = relation_names.reduce(attribute) do |level, relation_name|
|
|
92
|
+
level[relation_name] ||= {}
|
|
94
93
|
end
|
|
95
94
|
|
|
95
|
+
attribute_level[attribute_name] = type_cast_attribute_value(value)
|
|
96
96
|
attribute
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -128,6 +128,9 @@ module FulfilApi
|
|
|
128
128
|
configure_persistent_connection(http)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# Retries and other safeguards wrap every request, so they're added first
|
|
132
|
+
FulfilApi::Middleware.apply(connection, configuration)
|
|
133
|
+
|
|
131
134
|
# Configuration of the request middleware
|
|
132
135
|
connection.request :json
|
|
133
136
|
|
|
@@ -151,11 +154,9 @@ module FulfilApi
|
|
|
151
154
|
# Tunes the underlying Net::HTTP::Persistent connection.
|
|
152
155
|
#
|
|
153
156
|
# The `net_http_persistent` adapter forces `max_retries` to 0 on every
|
|
154
|
-
# request
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
# as a read timeout. The config block runs after the adapter zeroes the
|
|
158
|
-
# value, so this takes effect.
|
|
157
|
+
# request. Retries are handled by {FulfilApi::Middleware::Retry} instead, so
|
|
158
|
+
# `max_retries` is only restored when it's configured explicitly. The config
|
|
159
|
+
# block runs after the adapter zeroes the value, so this takes effect.
|
|
159
160
|
#
|
|
160
161
|
# @param http [Net::HTTP::Persistent] The live persistent connection.
|
|
161
162
|
# @return [void]
|
|
@@ -170,7 +171,10 @@ module FulfilApi
|
|
|
170
171
|
|
|
171
172
|
# @return [Array] The cache key identifying a unique connection.
|
|
172
173
|
def connection_cache_key
|
|
173
|
-
[
|
|
174
|
+
[
|
|
175
|
+
merchant_id, api_version, configuration.request_options, configuration.connection_options,
|
|
176
|
+
configuration.retry_options, configuration.circuit_breaker
|
|
177
|
+
]
|
|
174
178
|
end
|
|
175
179
|
|
|
176
180
|
# @param relative_path [String] The relative path to the API endpoint.
|
data/lib/fulfil_api/version.rb
CHANGED
data/lib/fulfil_api.rb
CHANGED
|
@@ -3,18 +3,29 @@
|
|
|
3
3
|
require "zeitwerk"
|
|
4
4
|
|
|
5
5
|
loader = Zeitwerk::Loader.for_gem
|
|
6
|
+
loader.inflector.inflect("oauth" => "OAuth")
|
|
7
|
+
loader.ignore("#{__dir__}/fulfil_api/engine.rb")
|
|
6
8
|
loader.ignore("#{__dir__}/fulfil_api/test_helper.rb")
|
|
9
|
+
loader.ignore("#{__dir__}/generators")
|
|
7
10
|
loader.setup
|
|
8
11
|
|
|
9
12
|
require "active_support"
|
|
13
|
+
require "active_support/cache"
|
|
10
14
|
require "active_support/core_ext/enumerable"
|
|
11
15
|
require "active_support/core_ext/hash/deep_merge"
|
|
12
16
|
require "active_support/core_ext/hash/indifferent_access"
|
|
13
17
|
require "active_support/core_ext/module/delegation"
|
|
14
18
|
require "active_support/core_ext/object/blank"
|
|
15
19
|
require "active_support/notifications"
|
|
20
|
+
require "faraday"
|
|
21
|
+
require "faraday/retry"
|
|
16
22
|
|
|
17
23
|
module FulfilApi
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
loader.eager_load
|
|
27
|
+
|
|
28
|
+
# The Rails-specific parts of the gem — the engine, its models, its controllers,
|
|
29
|
+
# and its generators — are only loaded when the gem is used from within a
|
|
30
|
+
# Rails application.
|
|
31
|
+
require "fulfil_api/engine" if defined?(Rails::Engine)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module FulfilApi
|
|
7
|
+
module Generators
|
|
8
|
+
# Sets a Rails application up for Fulfil's OAuth flow: it writes an
|
|
9
|
+
# initializer, adds the migration for {FulfilApi::Installation}, and mounts
|
|
10
|
+
# {FulfilApi::Engine}.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# bin/rails generate fulfil_api:install
|
|
14
|
+
class InstallGenerator < Rails::Generators::Base
|
|
15
|
+
include ActiveRecord::Generators::Migration
|
|
16
|
+
|
|
17
|
+
source_root File.expand_path("templates", __dir__)
|
|
18
|
+
|
|
19
|
+
desc "Installs the Fulfil OAuth initializer, migration, and engine route."
|
|
20
|
+
|
|
21
|
+
# @return [void]
|
|
22
|
+
def copy_initializer
|
|
23
|
+
template "fulfil_api.rb", "config/initializers/fulfil_api.rb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [void]
|
|
27
|
+
def create_migration_file
|
|
28
|
+
migration_template "create_fulfil_api_installations.rb",
|
|
29
|
+
"db/migrate/create_fulfil_api_installations.rb"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [void]
|
|
33
|
+
def mount_engine
|
|
34
|
+
route %(mount FulfilApi::Engine => "#{FulfilApi::OAuth::Configuration::DEFAULT_MOUNT_PATH}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [void]
|
|
38
|
+
def show_readme
|
|
39
|
+
say(remaining_steps)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# @return [String] The Active Record migration version to generate against.
|
|
45
|
+
def migration_version
|
|
46
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Nothing changes in the application until these are done, so an existing
|
|
50
|
+
# application can install the gem without switching to OAuth yet.
|
|
51
|
+
#
|
|
52
|
+
# @return [String]
|
|
53
|
+
def remaining_steps
|
|
54
|
+
<<~MESSAGE
|
|
55
|
+
|
|
56
|
+
Fulfil's OAuth flow is installed. To finish setting it up:
|
|
57
|
+
|
|
58
|
+
1. Run `bin/rails db:migrate` to create the fulfil_api_installations table.
|
|
59
|
+
2. Run `bin/rails db:encryption:init` and store the keys in your credentials,
|
|
60
|
+
unless Active Record encryption is already configured. The access tokens
|
|
61
|
+
are encrypted at rest.
|
|
62
|
+
3. Create an app on https://auth.fulfil.io/user/clients and whitelist
|
|
63
|
+
#{FulfilApi::OAuth::Configuration::DEFAULT_MOUNT_PATH}/callback as a redirection URL.
|
|
64
|
+
4. Fill in FULFIL_OAUTH_CLIENT_ID and FULFIL_OAUTH_CLIENT_SECRET.
|
|
65
|
+
5. Set config.oauth.parent_controller to the controller that authenticates
|
|
66
|
+
a request in this application. The OAuth flow refuses to run without it.
|
|
67
|
+
|
|
68
|
+
MESSAGE
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateFulfilApiInstallations < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :fulfil_api_installations do |t|
|
|
6
|
+
# An installation without an owner is the application-wide installation.
|
|
7
|
+
t.references :owner, polymorphic: true, null: true, index: false
|
|
8
|
+
|
|
9
|
+
t.string :merchant_id, null: false
|
|
10
|
+
|
|
11
|
+
# Fulfil grants the permanent token only in `offline_access` mode, and the
|
|
12
|
+
# short-lived one is the only token in `user_session` mode. Neither is
|
|
13
|
+
# required on its own; FulfilApi::Installation requires one of the two.
|
|
14
|
+
t.text :access_token
|
|
15
|
+
t.text :offline_access_token
|
|
16
|
+
|
|
17
|
+
# The gem fills the token type in rather than relying on Fulfil to send it.
|
|
18
|
+
t.string :token_type, null: false, default: "Bearer"
|
|
19
|
+
|
|
20
|
+
# Nullable because Active Record serialization writes an empty list as
|
|
21
|
+
# NULL and reads it back as one, so the column is never meaningfully
|
|
22
|
+
# blank even though it can be NULL.
|
|
23
|
+
t.text :scopes
|
|
24
|
+
|
|
25
|
+
# Describes the short-lived token only. A permanent token has no expiry:
|
|
26
|
+
# it is revoked by uninstalling the app, not by time.
|
|
27
|
+
t.datetime :expires_at
|
|
28
|
+
|
|
29
|
+
# Who walked through the authorization flow. Recorded for support, nothing
|
|
30
|
+
# in the gem depends on it, and Fulfil is free to leave parts of it out.
|
|
31
|
+
t.integer :associated_user_id
|
|
32
|
+
t.string :associated_user_email
|
|
33
|
+
t.string :associated_user_name
|
|
34
|
+
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# An owner is connected to a single Fulfil workspace. Databases treat NULLs
|
|
39
|
+
# as distinct, so this index does not constrain the application-wide
|
|
40
|
+
# installation; FulfilApi::Installation validates that case.
|
|
41
|
+
add_index :fulfil_api_installations, %i[owner_type owner_id], unique: true
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FulfilApi.configure do |config|
|
|
4
|
+
# The subdomain of the Fulfil workspace this application talks to. Leave it
|
|
5
|
+
# blank when every tenant brings its own workspace, and override
|
|
6
|
+
# `fulfil_merchant_id` in ApplicationController instead.
|
|
7
|
+
config.merchant_id = ENV.fetch("FULFIL_MERCHANT_ID", nil)
|
|
8
|
+
|
|
9
|
+
# The credentials of the app registered on https://auth.fulfil.io/user/clients.
|
|
10
|
+
config.oauth.client_id = ENV.fetch("FULFIL_OAUTH_CLIENT_ID", nil)
|
|
11
|
+
config.oauth.client_secret = ENV.fetch("FULFIL_OAUTH_CLIENT_SECRET", nil)
|
|
12
|
+
|
|
13
|
+
# The scopes to request. See https://developers.fulfil.io for the full list.
|
|
14
|
+
config.oauth.scopes = []
|
|
15
|
+
|
|
16
|
+
# `:offline_access` grants a permanent token that keeps working when no user
|
|
17
|
+
# is around, which is what background jobs need. `:user_session` grants a
|
|
18
|
+
# token that expires with the user's Fulfil session.
|
|
19
|
+
config.oauth.access_type = :offline_access
|
|
20
|
+
|
|
21
|
+
# Where the user ends up after installing the app, unless they were sent to
|
|
22
|
+
# the flow from somewhere else. Accepts a path or a callable taking the
|
|
23
|
+
# FulfilApi::Installation that was created.
|
|
24
|
+
config.oauth.after_install_path = "/"
|
|
25
|
+
|
|
26
|
+
# REQUIRED before the OAuth flow can be used. The engine's controllers inherit
|
|
27
|
+
# from this one, so it has to be a controller that authenticates the request
|
|
28
|
+
# and — in an application serving many merchants — establishes the current
|
|
29
|
+
# tenant. There is deliberately no default: an unauthenticated flow lets a
|
|
30
|
+
# stranger record their own Fulfil workspace over yours, and a flow outside
|
|
31
|
+
# the application's tenancy records installations against the wrong owner.
|
|
32
|
+
#
|
|
33
|
+
# config.oauth.parent_controller = "AuthenticatedController"
|
|
34
|
+
|
|
35
|
+
# Fulfil requires the redirect URL to be whitelisted on the app. It defaults
|
|
36
|
+
# to the engine's callback URL, which is derived from the incoming request.
|
|
37
|
+
# config.oauth.redirect_uri = "https://example.com/fulfil/callback"
|
|
38
|
+
end
|
metadata
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fulfil_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Vermaas
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
|
-
- - "
|
|
16
|
+
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '8.0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
|
-
- - "
|
|
23
|
+
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '8.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: faraday
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,6 +51,20 @@ dependencies:
|
|
|
52
51
|
- - "~>"
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: faraday-retry
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.2'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.2'
|
|
55
68
|
- !ruby/object:Gem::Dependency
|
|
56
69
|
name: zeitwerk
|
|
57
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -80,14 +93,29 @@ files:
|
|
|
80
93
|
- LICENSE.txt
|
|
81
94
|
- README.md
|
|
82
95
|
- Rakefile
|
|
96
|
+
- app/controllers/concerns/fulfil_api/authenticated.rb
|
|
97
|
+
- app/controllers/concerns/fulfil_api/installation_scoped.rb
|
|
98
|
+
- app/controllers/fulfil_api/application_controller.rb
|
|
99
|
+
- app/controllers/fulfil_api/installations_controller.rb
|
|
100
|
+
- app/models/concerns/fulfil_api/installable.rb
|
|
101
|
+
- app/models/fulfil_api/installation.rb
|
|
102
|
+
- config/routes.rb
|
|
83
103
|
- lib/fulfil_api.rb
|
|
84
104
|
- lib/fulfil_api/access_token.rb
|
|
105
|
+
- lib/fulfil_api/circuit_open.rb
|
|
85
106
|
- lib/fulfil_api/client.rb
|
|
86
107
|
- lib/fulfil_api/configuration.rb
|
|
87
108
|
- lib/fulfil_api/customer_shipment.rb
|
|
109
|
+
- lib/fulfil_api/engine.rb
|
|
88
110
|
- lib/fulfil_api/error.rb
|
|
89
111
|
- lib/fulfil_api/error/notifiable.rb
|
|
90
112
|
- lib/fulfil_api/http_error.rb
|
|
113
|
+
- lib/fulfil_api/middleware.rb
|
|
114
|
+
- lib/fulfil_api/middleware/circuit_breaker.rb
|
|
115
|
+
- lib/fulfil_api/oauth.rb
|
|
116
|
+
- lib/fulfil_api/oauth/authorization.rb
|
|
117
|
+
- lib/fulfil_api/oauth/configuration.rb
|
|
118
|
+
- lib/fulfil_api/oauth/token.rb
|
|
91
119
|
- lib/fulfil_api/relation.rb
|
|
92
120
|
- lib/fulfil_api/relation/batchable.rb
|
|
93
121
|
- lib/fulfil_api/relation/countable.rb
|
|
@@ -105,6 +133,9 @@ files:
|
|
|
105
133
|
- lib/fulfil_api/test_helper.rb
|
|
106
134
|
- lib/fulfil_api/tpl_client.rb
|
|
107
135
|
- lib/fulfil_api/version.rb
|
|
136
|
+
- lib/generators/fulfil_api/install/install_generator.rb
|
|
137
|
+
- lib/generators/fulfil_api/install/templates/create_fulfil_api_installations.rb
|
|
138
|
+
- lib/generators/fulfil_api/install/templates/fulfil_api.rb
|
|
108
139
|
- sig/fulfil_api.rbs
|
|
109
140
|
homepage: https://www.github.com/codeturebv/fulfil_api
|
|
110
141
|
licenses:
|
|
@@ -114,7 +145,13 @@ metadata:
|
|
|
114
145
|
source_code_uri: https://www.github.com/codeturebv/fulfil_api
|
|
115
146
|
changelog_uri: https://www.github.com/codeturebv/fulfil_api/blob/main/CHANGELOG.md
|
|
116
147
|
rubygems_mfa_required: 'true'
|
|
117
|
-
post_install_message:
|
|
148
|
+
post_install_message: |+
|
|
149
|
+
Using fulfil_api in a Rails application? Run the install generator to set up
|
|
150
|
+
Fulfil's OAuth flow, then run the migration it adds:
|
|
151
|
+
|
|
152
|
+
bin/rails generate fulfil_api:install
|
|
153
|
+
bin/rails db:migrate
|
|
154
|
+
|
|
118
155
|
rdoc_options: []
|
|
119
156
|
require_paths:
|
|
120
157
|
- lib
|
|
@@ -122,15 +159,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
122
159
|
requirements:
|
|
123
160
|
- - ">="
|
|
124
161
|
- !ruby/object:Gem::Version
|
|
125
|
-
version: 3.
|
|
162
|
+
version: 3.2.0
|
|
126
163
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
164
|
requirements:
|
|
128
165
|
- - ">="
|
|
129
166
|
- !ruby/object:Gem::Version
|
|
130
167
|
version: '0'
|
|
131
168
|
requirements: []
|
|
132
|
-
rubygems_version:
|
|
133
|
-
signing_key:
|
|
169
|
+
rubygems_version: 4.0.20
|
|
134
170
|
specification_version: 4
|
|
135
171
|
summary: A HTTP client to interact the Fulfil.io API
|
|
136
172
|
test_files: []
|
|
173
|
+
...
|