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.
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task default: :test
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'test'
8
+ t.pattern = "test/**/*_test.rb"
9
+ end
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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