procore 0.6.7

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