reso_api 1.8.9 → 1.8.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b26043f928133bc54314e5b591600fc425a8302a883c0578bb2597d9f94e03e0
4
- data.tar.gz: 816b9c11a937a44e7ea0644ddf1f724c43d7d77e9104ca02f596455769621b9a
3
+ metadata.gz: e81ebb68ef64ed1eb595cf0a92fd03044c03b28e84d40a31fa49e7660ab65a08
4
+ data.tar.gz: 508a035516cbbf60b0d7d606a6277edcf0e72cfb063cf5f2497761f9e1b71d7f
5
5
  SHA512:
6
- metadata.gz: aa952c785b3e828357423537c8d0b161644ed8d0ca9eefdd4b22e65c0ad7e4dff22e233a8518397f2fb356e38d8c4a3066b08f97df622b9c088d48c3f0fa7976
7
- data.tar.gz: 5f2e5b8e68e89554201e323fb4fcf94ef02242fdf89d545cf250a211d1e9f572e9324dda3d500c70eb16d4b9eda906f0c74baea674500e94679c8bb9c1215375
6
+ metadata.gz: 426229c6c88d7cf4e91f6237989a94b91086af2ef9ad04063638c5ac230851fdccd7a9520d6c30c587cb43009eb93505b93136bb40d2d5894d443fadce286a97
7
+ data.tar.gz: 56f7d6afc8f864cec994b91a1ca8906ef7b48e9209d51fa7e4a8974e03210e254d77f40ccd8e4a238653fbbc47497d6139f3d80d11f161033ac1bb9a5187babc
@@ -5,6 +5,7 @@ module RESO
5
5
  require 'net/http'
6
6
  require 'oauth2'
7
7
  require 'json'
8
+ require 'jwt'
8
9
  require 'tmpdir'
9
10
 
10
11
  attr_accessor :access_token, :client_id, :client_secret, :auth_url, :base_url, :scope, :osn
@@ -109,7 +110,31 @@ module RESO
109
110
  end
110
111
 
111
112
  def auth_token
112
- access_token.presence ? access_token : oauth2_token
113
+ # If access_token is provided, check if it's expired
114
+ if access_token.present?
115
+ # Try to decode as JWT to check expiration
116
+ begin
117
+ token = JWT.decode(access_token, nil, false)
118
+ exp_timestamp = Hash(token.try(:first))["exp"].to_s
119
+ expiration = DateTime.strptime(exp_timestamp, '%s').utc rescue nil
120
+
121
+ # If token is expired and we have OAuth credentials, get a fresh token
122
+ if expiration && expiration <= DateTime.now.utc && can_use_oauth?
123
+ return oauth2_token
124
+ end
125
+ rescue JWT::DecodeError
126
+ # Not a JWT token, just use it as-is
127
+ end
128
+
129
+ return access_token
130
+ end
131
+
132
+ # No access_token provided, use OAuth flow
133
+ oauth2_token
134
+ end
135
+
136
+ def can_use_oauth?
137
+ client_id.present? && client_secret.present? && auth_url.present?
113
138
  end
114
139
 
115
140
  def oauth2_client
@@ -136,9 +161,19 @@ module RESO
136
161
  end
137
162
 
138
163
  def fresh_oauth2_payload
139
- @oauth2_payload = oauth2_client.client_credentials.get_token('client_id' => client_id, 'client_secret' => client_secret, 'scope' => scope.presence)
140
- File.write(oauth2_token_path, @oauth2_payload.to_hash.to_json)
141
- return @oauth2_payload
164
+ begin
165
+ @oauth2_payload = oauth2_client.client_credentials.get_token('client_id' => client_id, 'client_secret' => client_secret, 'scope' => scope.presence)
166
+ File.write(oauth2_token_path, @oauth2_payload.to_hash.to_json)
167
+ return @oauth2_payload
168
+ rescue OAuth2::Error => e
169
+ # Provide detailed error message for OAuth failures
170
+ error_details = "OAuth token refresh failed for #{base_url}"
171
+ error_details += "\n Scope attempted: #{scope.inspect}"
172
+ error_details += "\n OAuth error: #{e.message}"
173
+ raise StandardError, error_details
174
+ rescue => e
175
+ raise StandardError, "Failed to refresh OAuth token: #{e.message}"
176
+ end
142
177
  end
143
178
 
144
179
  def oauth2_token_path
@@ -151,13 +186,43 @@ module RESO
151
186
 
152
187
  def get_oauth2_payload
153
188
  if File.exist?(oauth2_token_path)
154
- persisted = File.read(oauth2_token_path)
155
- payload = OAuth2::AccessToken.from_hash(oauth2_client, JSON.parse(persisted))
156
- else
189
+ begin
190
+ persisted = File.read(oauth2_token_path)
191
+ parsed = JSON.parse(persisted)
192
+
193
+ # Check if the persisted data is a valid token (has access_token or token field)
194
+ if parsed['access_token'].present? || parsed['token'].present?
195
+ payload = OAuth2::AccessToken.from_hash(oauth2_client, parsed)
196
+
197
+ # Verify the payload actually has a token
198
+ if payload.token.present?
199
+ return payload
200
+ end
201
+ end
202
+
203
+ # If we get here, the cached token is invalid - delete it and get fresh
204
+ File.delete(oauth2_token_path)
205
+ rescue JSON::ParserError, StandardError => e
206
+ # If there's any error reading/parsing the cached token, delete it
207
+ File.delete(oauth2_token_path) if File.exist?(oauth2_token_path)
208
+ end
209
+ end
210
+
211
+ # Get fresh token
212
+ begin
157
213
  payload = oauth2_client.client_credentials.get_token('client_id' => client_id, 'client_secret' => client_secret, 'scope' => scope.presence)
158
214
  File.write(oauth2_token_path, payload.to_hash.to_json)
215
+ return payload
216
+ rescue OAuth2::Error => e
217
+ # Clean up any bad cached token
218
+ File.delete(oauth2_token_path) if File.exist?(oauth2_token_path)
219
+
220
+ # Provide detailed error message
221
+ error_details = "OAuth token request failed for #{base_url}"
222
+ error_details += "\n Scope attempted: #{scope.inspect}"
223
+ error_details += "\n OAuth error: #{e.message}"
224
+ raise StandardError, error_details
159
225
  end
160
- return payload
161
226
  end
162
227
 
163
228
  def uri_for_endpoint endpoint
@@ -189,8 +254,9 @@ module RESO
189
254
  fresh_oauth2_payload
190
255
  raise StandardError
191
256
  elsif response.is_a?(Hash) && response.has_key?("error")
192
- puts "Error: #{response.inspect}" if debug
193
- raise StandardError
257
+ error_msg = response.inspect
258
+ puts "Error: #{error_msg}" if debug
259
+ raise StandardError, error_msg
194
260
  elsif response.is_a?(Hash) && response.has_key?("retry-after")
195
261
  puts "Error: Retrying in #{response["retry-after"].to_i}} seconds." if debug
196
262
  sleep response["retry-after"].to_i
@@ -1,3 +1,3 @@
1
1
  module ResoApi
2
- VERSION = "1.8.9"
2
+ VERSION = "1.8.11"
3
3
  end
data/reso_api.gemspec CHANGED
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency "rspec", "~> 3.0"
26
26
  spec.add_dependency 'activesupport'
27
27
  spec.add_dependency "oauth2"
28
+ spec.add_dependency "jwt"
28
29
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reso_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.9
4
+ version: 1.8.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Edlund
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: jwt
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
82
96
  description: Ruby wrapper for easy interaction with a RESO Web API compliant server.
83
97
  email:
84
98
  - medlund@mac.com
@@ -121,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
135
  - !ruby/object:Gem::Version
122
136
  version: '0'
123
137
  requirements: []
124
- rubygems_version: 3.6.2
138
+ rubygems_version: 3.7.2
125
139
  specification_version: 4
126
140
  summary: RESO Web API Wrapper
127
141
  test_files: []