procore 0.6.7
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 +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +27 -0
- data/CHANGELOG.md +46 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +407 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/procore.rb +21 -0
- data/lib/procore/auth/access_token_credentials.rb +35 -0
- data/lib/procore/auth/client_credentials.rb +52 -0
- data/lib/procore/auth/stores/active_record.rb +41 -0
- data/lib/procore/auth/stores/file.rb +32 -0
- data/lib/procore/auth/stores/memory.rb +29 -0
- data/lib/procore/auth/stores/redis.rb +42 -0
- data/lib/procore/auth/stores/session.rb +38 -0
- data/lib/procore/auth/token.rb +20 -0
- data/lib/procore/client.rb +86 -0
- data/lib/procore/configuration.rb +88 -0
- data/lib/procore/defaults.rb +19 -0
- data/lib/procore/errors.rb +63 -0
- data/lib/procore/requestable.rb +217 -0
- data/lib/procore/response.rb +78 -0
- data/lib/procore/util.rb +52 -0
- data/lib/procore/version.rb +3 -0
- data/procore.gemspec +37 -0
- metadata +239 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "procore"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/procore.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Procore
|
2
|
+
end
|
3
|
+
|
4
|
+
require "logger"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
require "procore/auth/access_token_credentials"
|
8
|
+
require "procore/auth/client_credentials"
|
9
|
+
require "procore/auth/stores/active_record"
|
10
|
+
require "procore/auth/stores/file"
|
11
|
+
require "procore/auth/stores/memory"
|
12
|
+
require "procore/auth/stores/redis"
|
13
|
+
require "procore/auth/stores/session"
|
14
|
+
require "procore/auth/token"
|
15
|
+
require "procore/client"
|
16
|
+
require "procore/configuration"
|
17
|
+
require "procore/defaults"
|
18
|
+
require "procore/errors"
|
19
|
+
require "procore/response"
|
20
|
+
require "procore/util"
|
21
|
+
require "procore/version"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "oauth2"
|
2
|
+
|
3
|
+
module Procore
|
4
|
+
module Auth
|
5
|
+
class AccessTokenCredentials
|
6
|
+
attr_reader :client_id, :client_secret, :host
|
7
|
+
def initialize(client_id:, client_secret:, host:)
|
8
|
+
@client_id = client_id
|
9
|
+
@client_secret = client_secret
|
10
|
+
@host = host
|
11
|
+
end
|
12
|
+
|
13
|
+
def refresh(token:, refresh:)
|
14
|
+
token = OAuth2::AccessToken.new(client, token, refresh_token: refresh)
|
15
|
+
new_token = token.refresh!
|
16
|
+
|
17
|
+
Procore::Auth::Token.new(
|
18
|
+
access_token: new_token.token,
|
19
|
+
refresh_token: new_token.refresh_token,
|
20
|
+
expires_at: new_token.expires_at,
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def client
|
27
|
+
OAuth2::Client.new(
|
28
|
+
client_id,
|
29
|
+
client_secret,
|
30
|
+
site: "#{host}/oauth/token",
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "oauth2"
|
2
|
+
|
3
|
+
module Procore
|
4
|
+
module Auth
|
5
|
+
class ClientCredentials
|
6
|
+
attr_reader :client_id, :client_secret, :host
|
7
|
+
def initialize(client_id:, client_secret:, host:)
|
8
|
+
unless client_id && client_secret
|
9
|
+
raise OAuthError.new("No client_id or client_secret provided.")
|
10
|
+
end
|
11
|
+
|
12
|
+
@client_id = client_id
|
13
|
+
@client_secret = client_secret
|
14
|
+
@host = host
|
15
|
+
end
|
16
|
+
|
17
|
+
def refresh(token: nil, refresh: nil)
|
18
|
+
new_token = client
|
19
|
+
.client_credentials
|
20
|
+
.get_token({}, { auth_scheme: :request_body })
|
21
|
+
|
22
|
+
Procore::Auth::Token.new(
|
23
|
+
access_token: new_token.token,
|
24
|
+
refresh_token: new_token.refresh_token,
|
25
|
+
expires_at: new_token.expires_at,
|
26
|
+
)
|
27
|
+
|
28
|
+
rescue OAuth2::Error => e
|
29
|
+
raise OAuthError.new(e.description, response: e.response)
|
30
|
+
|
31
|
+
rescue Faraday::ConnectionFailed => e
|
32
|
+
raise APIConnectionError.new("Connection Error: #{e.message}")
|
33
|
+
|
34
|
+
rescue URI::BadURIError
|
35
|
+
raise OAuthError.new(
|
36
|
+
"Host is not a valid URI. Check your host option to make sure it " \
|
37
|
+
"is a properly formed url"
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def client
|
44
|
+
OAuth2::Client.new(
|
45
|
+
client_id,
|
46
|
+
client_secret,
|
47
|
+
site: "#{host}/oauth/token",
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Procore
|
2
|
+
module Auth
|
3
|
+
module Stores
|
4
|
+
class ActiveRecord
|
5
|
+
attr_reader :object
|
6
|
+
|
7
|
+
def initialize(object:)
|
8
|
+
@object = object
|
9
|
+
end
|
10
|
+
|
11
|
+
def save(token)
|
12
|
+
object.update(
|
13
|
+
access_token: token.access_token,
|
14
|
+
refresh_token: token.refresh_token,
|
15
|
+
expires_at: token.expires_at,
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch
|
20
|
+
Procore::Auth::Token.new(
|
21
|
+
access_token: object.access_token,
|
22
|
+
refresh_token: object.refresh_token,
|
23
|
+
expires_at: object.expires_at,
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete
|
28
|
+
object.update(
|
29
|
+
access_token: nil,
|
30
|
+
expires_at: nil,
|
31
|
+
refresh_token: nil,
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"Active Record, Object: #{object.class} #{object.id}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "yaml/store"
|
2
|
+
|
3
|
+
module Procore
|
4
|
+
module Auth
|
5
|
+
module Stores
|
6
|
+
class File
|
7
|
+
attr_reader :key, :path
|
8
|
+
def initialize(key:, path:)
|
9
|
+
@key = key
|
10
|
+
@path = path
|
11
|
+
@store = YAML::Store.new(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def save(token)
|
15
|
+
@store.transaction { @store[key] = token }
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch
|
19
|
+
@store.transaction { @store[key] }
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete
|
23
|
+
@store.transaction { @store.delete(key) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
"File, Key: #{key}, Path: #{path}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Procore
|
2
|
+
module Auth
|
3
|
+
module Stores
|
4
|
+
class Memory
|
5
|
+
attr_reader :key, :store
|
6
|
+
def initialize(key:)
|
7
|
+
@key = key
|
8
|
+
@store = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def save(token)
|
12
|
+
@store[key] = token
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch
|
16
|
+
@store[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete
|
20
|
+
@store.delete(key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"Memory, Key: #{key}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Procore
|
2
|
+
module Auth
|
3
|
+
module Stores
|
4
|
+
class Redis
|
5
|
+
attr_reader :key, :redis
|
6
|
+
def initialize(key:, redis:)
|
7
|
+
@key = key
|
8
|
+
@redis = redis
|
9
|
+
end
|
10
|
+
|
11
|
+
def save(token)
|
12
|
+
redis.set(redis_key, token.to_json)
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch
|
16
|
+
return if redis.get(redis_key).empty?
|
17
|
+
|
18
|
+
token = JSON.parse(redis.get(redis_key))
|
19
|
+
Procore::Auth::Token.new(
|
20
|
+
access_token: token["access_token"],
|
21
|
+
refresh_token: token["refresh_token"],
|
22
|
+
expires_at: token["expires_at"],
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete
|
27
|
+
redis.set(redis_key, nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"Redis, Key: #{redis_key}"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def redis_key
|
37
|
+
"procore-redis-#{key}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Procore
|
2
|
+
module Auth
|
3
|
+
module Stores
|
4
|
+
SESSION_KEY = "procore_token".to_sym
|
5
|
+
|
6
|
+
class Session
|
7
|
+
attr_reader :session, :key
|
8
|
+
def initialize(session:, key: SESSION_KEY)
|
9
|
+
@session = session
|
10
|
+
@key = key
|
11
|
+
end
|
12
|
+
|
13
|
+
def save(token)
|
14
|
+
session[key] = token.to_json
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch
|
18
|
+
return if session[key].nil?
|
19
|
+
|
20
|
+
token = JSON.parse(session[key])
|
21
|
+
Procore::Auth::Token.new(
|
22
|
+
access_token: token["access_token"],
|
23
|
+
refresh_token: token["refresh_token"],
|
24
|
+
expires_at: token["expires_at"],
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete
|
29
|
+
session[key] = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
"Session, Key: #{key}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Procore
|
2
|
+
module Auth
|
3
|
+
class Token
|
4
|
+
attr_reader :access_token, :refresh_token, :expires_at
|
5
|
+
def initialize(access_token:, refresh_token:, expires_at:)
|
6
|
+
@access_token = access_token
|
7
|
+
@refresh_token = refresh_token
|
8
|
+
@expires_at = expires_at
|
9
|
+
end
|
10
|
+
|
11
|
+
def invalid?
|
12
|
+
access_token.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def expired?
|
16
|
+
expires_at.to_i < Time.now.to_i
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "procore/requestable"
|
2
|
+
|
3
|
+
module Procore
|
4
|
+
# Main class end users interact with. An instance of a client can call out
|
5
|
+
# the Procore API using methods matching standard HTTP verbs #get, #post,
|
6
|
+
# #patch, #delete.
|
7
|
+
#
|
8
|
+
# @example Creating a new client:
|
9
|
+
# store = Procore::Auth::Stores::Session.new(session: session)
|
10
|
+
# client = Procore::Client.new(
|
11
|
+
# client_id: Rails.application.secrets.procore_client_id,
|
12
|
+
# client_secret: Rails.application.secrets.procore_secret_id,
|
13
|
+
# store: store
|
14
|
+
# )
|
15
|
+
#
|
16
|
+
# client.get("me").body #=> { id: 5, email: "person@example.com" }
|
17
|
+
class Client
|
18
|
+
include Procore::Requestable
|
19
|
+
|
20
|
+
attr_reader :options, :store
|
21
|
+
|
22
|
+
# @param client_id [String] Client ID issued from Procore
|
23
|
+
# @param client_secret [String] Client Secret issued from Procore
|
24
|
+
# @param store [Auth::Store] A store to use for saving, updating and
|
25
|
+
# refreshing tokens
|
26
|
+
# @param options [Hash] options to configure the client with
|
27
|
+
# @option options [String] :host Endpoint to use for the API. Defaults to
|
28
|
+
# Configuration.host
|
29
|
+
# @option options [String] :user_agent User Agent string to send along with
|
30
|
+
# the request. Defaults to Configuration.user_agent
|
31
|
+
def initialize(client_id:, client_secret:, store:, options: {})
|
32
|
+
@options = Procore::Defaults::client_options.merge(options)
|
33
|
+
@credentials = Procore::Auth::AccessTokenCredentials.new(
|
34
|
+
client_id: client_id,
|
35
|
+
client_secret: client_secret,
|
36
|
+
host: @options[:host],
|
37
|
+
)
|
38
|
+
@store = store
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def base_api_path
|
44
|
+
"#{options[:host]}/vapid"
|
45
|
+
end
|
46
|
+
|
47
|
+
# @raise [OAuthError] if the store does not have a token stored in it prior
|
48
|
+
# to making a request.
|
49
|
+
# @raise [OAuthError] if a token cannot be refreshed.
|
50
|
+
# @raise [OAuthError] if incorrect credentials have been supplied.
|
51
|
+
def access_token
|
52
|
+
token = store.fetch
|
53
|
+
|
54
|
+
if token.nil? || token.invalid?
|
55
|
+
raise Procore::OAuthError.new(
|
56
|
+
"Unable to retreive an access token from the store. Double check " \
|
57
|
+
"your store configuration and make sure to correctly store a token " \
|
58
|
+
"before attempting to make API requests"
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
if token.expired?
|
63
|
+
Util.log_info("Token Expired", store: store)
|
64
|
+
begin
|
65
|
+
token = @credentials.refresh(
|
66
|
+
token: token.access_token,
|
67
|
+
refresh: token.refresh_token,
|
68
|
+
)
|
69
|
+
|
70
|
+
Util.log_info("Token Refresh Successful", store: store)
|
71
|
+
store.save(token)
|
72
|
+
rescue RuntimeError
|
73
|
+
Util.log_error("Token Refresh Failed", store: store)
|
74
|
+
raise Procore::OAuthError.new(
|
75
|
+
"Unable to refresh the access token. Perhaps the Procore API is " \
|
76
|
+
"down or the your access token store is misconfigured. Either " \
|
77
|
+
"way, you should clear the store and prompt the user to sign in " \
|
78
|
+
"again."
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
token.access_token
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "procore/defaults"
|
2
|
+
|
3
|
+
module Procore
|
4
|
+
# Yields the configuration so the end user can set multiple attributes at
|
5
|
+
# once.
|
6
|
+
#
|
7
|
+
# @example Within config/initializers/procore.rb
|
8
|
+
# Procore.configure do |config|
|
9
|
+
# config.timeout = 5.0
|
10
|
+
# config.user_agent = MyApp
|
11
|
+
# end
|
12
|
+
def self.configure
|
13
|
+
yield(configuration)
|
14
|
+
end
|
15
|
+
|
16
|
+
# The current configuration for the gem.
|
17
|
+
#
|
18
|
+
# @return [Configuration]
|
19
|
+
def self.configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Holds the configuration for the Procore gem.
|
24
|
+
class Configuration
|
25
|
+
# @!attribute [rw] host
|
26
|
+
# @note defaults to Defaults::API_ENDPOINT
|
27
|
+
#
|
28
|
+
# Base API host name. Alter this depending on your environment - in a
|
29
|
+
# staging or test environment you may want to point this at a sandbox
|
30
|
+
# instead of production.
|
31
|
+
#
|
32
|
+
# @return [String]
|
33
|
+
attr_accessor :host
|
34
|
+
|
35
|
+
# @!attribute [rw] logger
|
36
|
+
# @note defaults to nil
|
37
|
+
#
|
38
|
+
# Instance of a Logger. This gem will log information about requests,
|
39
|
+
# responses and other things it might be doing. In a Rails application it
|
40
|
+
# should be set to Rails.logger
|
41
|
+
#
|
42
|
+
# @return [Logger, nil]
|
43
|
+
attr_accessor :logger
|
44
|
+
|
45
|
+
# @!attribute [rw] max_retries
|
46
|
+
# @note Defaults to 1
|
47
|
+
#
|
48
|
+
# Number of times to retry a failed API call. Reasons an API call
|
49
|
+
# could potentially fail:
|
50
|
+
# 1. Service is briefly down or unreachable
|
51
|
+
# 2. Timeout hit - service is experiencing immense load or mid restart
|
52
|
+
# 3. Because computers
|
53
|
+
#
|
54
|
+
# Would recommend 3-5 for production use. Has exponential backoff - first
|
55
|
+
# request waits a 1.5s after a failure, next one 2.25s, next one 3.375s,
|
56
|
+
# 5.0, etc.
|
57
|
+
#
|
58
|
+
# @return [Integer]
|
59
|
+
attr_accessor :max_retries
|
60
|
+
|
61
|
+
# @!attribute [rw] timeout
|
62
|
+
# @note defaults to 1.0
|
63
|
+
#
|
64
|
+
# Threshold for canceling an API request. If a request takes longer
|
65
|
+
# than this value it will automatically cancel.
|
66
|
+
#
|
67
|
+
# @return [Float]
|
68
|
+
attr_accessor :timeout
|
69
|
+
|
70
|
+
# @!attribute [rw] user_agent
|
71
|
+
# @note defaults to Defaults::USER_AGENT
|
72
|
+
#
|
73
|
+
# User Agent sent with each API request. API requests must have a user
|
74
|
+
# agent set. It is recomended to set the user agent to the name of your
|
75
|
+
# application.
|
76
|
+
#
|
77
|
+
# @return [String]
|
78
|
+
attr_accessor :user_agent
|
79
|
+
|
80
|
+
def initialize
|
81
|
+
@host = Procore::Defaults::API_ENDPOINT
|
82
|
+
@logger = nil
|
83
|
+
@max_retries = 1
|
84
|
+
@timeout = 1.0
|
85
|
+
@user_agent = Procore::Defaults::USER_AGENT
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|