procore 0.6.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|