omniauth-clover-oauth2 1.0.0 → 1.1.1
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 +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +94 -1
- data/lib/omniauth/clover_oauth2/token_client.rb +215 -0
- data/lib/omniauth/clover_oauth2/version.rb +1 -1
- data/lib/omniauth-clover-oauth2.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb709fb251b03d2c45fce7d93faeaaea36f310b2f6fa3250122714834df7a7c1
|
|
4
|
+
data.tar.gz: eda34b1fe80cfb8b5ef604e161c82a3490b914042f8579cb19e9e98ad54d8d71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70e02544685ecc4e933c4ff32509563bd42790915b1993da626d684ebe0d906524f13328a0d5c0be0f20151322268f38ae6c12cdf379d306cb9ddf87f35a7666
|
|
7
|
+
data.tar.gz: 6de45329ff4570f9cfef702a162a8375b04e4f37f38ad5456736d74ef5721e03c3a53ca389b289f790c535296a9eeaecd5a34d54f58b457a7a723b861c110576
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [1.
|
|
8
|
+
## [1.1.1] - 2026-01-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- TokenClient now uses correct Clover OAuth v2 refresh endpoint (`/oauth/v2/refresh`)
|
|
13
|
+
- Fixed expiration parsing to handle Unix timestamps from Clover
|
|
14
|
+
|
|
15
|
+
## [1.1.0] - 2026-01-30
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- TokenClient class for easy token refresh in Rails apps
|
|
20
|
+
- `refresh_token` method with result object pattern
|
|
21
|
+
- `token_expired?` helper with configurable buffer
|
|
22
|
+
- Comprehensive RSpec tests for TokenClient
|
|
23
|
+
|
|
24
|
+
## [1.0.1] - 2026-01-30
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Minor documentation and metadata updates
|
|
29
|
+
|
|
30
|
+
## [1.0.0] - 2026-01-30
|
|
9
31
|
|
|
10
32
|
### Added
|
|
11
33
|
|
data/README.md
CHANGED
|
@@ -231,7 +231,100 @@ end
|
|
|
231
231
|
|
|
232
232
|
## Token Refresh
|
|
233
233
|
|
|
234
|
-
Clover access tokens expire (typically after 1 year, but check your app settings).
|
|
234
|
+
Clover access tokens expire (typically after 1 year, but check your app settings). This gem includes a `TokenClient` class to easily refresh tokens in your Rails app.
|
|
235
|
+
|
|
236
|
+
### Basic Usage
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# Create a client instance
|
|
240
|
+
client = OmniAuth::CloverOauth2::TokenClient.new(
|
|
241
|
+
client_id: ENV['CLOVER_CLIENT_ID'],
|
|
242
|
+
client_secret: ENV['CLOVER_CLIENT_SECRET'],
|
|
243
|
+
sandbox: Rails.env.development?
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Refresh an expired token
|
|
247
|
+
result = client.refresh_token(user.clover_refresh_token)
|
|
248
|
+
|
|
249
|
+
if result.success?
|
|
250
|
+
user.update!(
|
|
251
|
+
clover_access_token: result.access_token,
|
|
252
|
+
clover_refresh_token: result.refresh_token,
|
|
253
|
+
clover_token_expires_at: Time.at(result.expires_at)
|
|
254
|
+
)
|
|
255
|
+
else
|
|
256
|
+
Rails.logger.error "Token refresh failed: #{result.error}"
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Check Token Expiration
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
# Check if token is expired (with 5-minute buffer by default)
|
|
264
|
+
client.token_expired?(user.clover_token_expires_at)
|
|
265
|
+
|
|
266
|
+
# Custom buffer (e.g., refresh 1 hour before expiry)
|
|
267
|
+
client.token_expired?(user.clover_token_expires_at, buffer_seconds: 3600)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Rails Service Example
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
# app/services/clover_api_service.rb
|
|
274
|
+
class CloverApiService
|
|
275
|
+
def initialize(user)
|
|
276
|
+
@user = user
|
|
277
|
+
@client = OmniAuth::CloverOauth2::TokenClient.new(
|
|
278
|
+
client_id: ENV['CLOVER_CLIENT_ID'],
|
|
279
|
+
client_secret: ENV['CLOVER_CLIENT_SECRET'],
|
|
280
|
+
sandbox: !Rails.env.production?
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def with_valid_token
|
|
285
|
+
refresh_if_expired!
|
|
286
|
+
yield @user.clover_access_token
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def refresh_if_expired!
|
|
292
|
+
return unless @client.token_expired?(@user.clover_token_expires_at)
|
|
293
|
+
|
|
294
|
+
result = @client.refresh_token(@user.clover_refresh_token)
|
|
295
|
+
raise "Token refresh failed: #{result.error}" unless result.success?
|
|
296
|
+
|
|
297
|
+
@user.update!(
|
|
298
|
+
clover_access_token: result.access_token,
|
|
299
|
+
clover_refresh_token: result.refresh_token,
|
|
300
|
+
clover_token_expires_at: Time.at(result.expires_at)
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Usage
|
|
306
|
+
CloverApiService.new(current_user).with_valid_token do |token|
|
|
307
|
+
# Make API calls with valid token
|
|
308
|
+
response = Faraday.get("https://api.clover.com/v3/merchants/#{merchant_id}") do |req|
|
|
309
|
+
req.headers['Authorization'] = "Bearer #{token}"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### TokenResult Object
|
|
315
|
+
|
|
316
|
+
The `refresh_token` method returns a `TokenResult` object with:
|
|
317
|
+
|
|
318
|
+
| Method | Description |
|
|
319
|
+
|--------|-------------|
|
|
320
|
+
| `success?` | Returns `true` if refresh succeeded |
|
|
321
|
+
| `failure?` | Returns `true` if refresh failed |
|
|
322
|
+
| `access_token` | The new access token |
|
|
323
|
+
| `refresh_token` | The new refresh token |
|
|
324
|
+
| `expires_at` | Unix timestamp when token expires |
|
|
325
|
+
| `expires_in` | Seconds until token expires |
|
|
326
|
+
| `error` | Error message if failed |
|
|
327
|
+
| `raw_response` | Full response hash from Clover |
|
|
235
328
|
|
|
236
329
|
## Contributing
|
|
237
330
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module OmniAuth
|
|
7
|
+
module CloverOauth2
|
|
8
|
+
# Client for managing Clover OAuth2 tokens
|
|
9
|
+
#
|
|
10
|
+
# This class provides an easy way to refresh expired tokens in your Rails app.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# client = OmniAuth::CloverOauth2::TokenClient.new(
|
|
14
|
+
# client_id: ENV['CLOVER_CLIENT_ID'],
|
|
15
|
+
# client_secret: ENV['CLOVER_CLIENT_SECRET'],
|
|
16
|
+
# sandbox: Rails.env.development?
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# # Refresh an expired token
|
|
20
|
+
# result = client.refresh_token(user.clover_refresh_token)
|
|
21
|
+
# if result.success?
|
|
22
|
+
# user.update!(
|
|
23
|
+
# clover_access_token: result.access_token,
|
|
24
|
+
# clover_refresh_token: result.refresh_token,
|
|
25
|
+
# clover_token_expires_at: result.expires_at
|
|
26
|
+
# )
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example With automatic token refresh in a service
|
|
30
|
+
# class CloverApiService
|
|
31
|
+
# def initialize(user)
|
|
32
|
+
# @user = user
|
|
33
|
+
# @client = OmniAuth::CloverOauth2::TokenClient.new(
|
|
34
|
+
# client_id: ENV['CLOVER_CLIENT_ID'],
|
|
35
|
+
# client_secret: ENV['CLOVER_CLIENT_SECRET']
|
|
36
|
+
# )
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# def with_valid_token
|
|
40
|
+
# refresh_if_expired!
|
|
41
|
+
# yield @user.clover_access_token
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# private
|
|
45
|
+
#
|
|
46
|
+
# def refresh_if_expired!
|
|
47
|
+
# return unless @user.clover_token_expired?
|
|
48
|
+
#
|
|
49
|
+
# result = @client.refresh_token(@user.clover_refresh_token)
|
|
50
|
+
# raise "Token refresh failed: #{result.error}" unless result.success?
|
|
51
|
+
#
|
|
52
|
+
# @user.update!(
|
|
53
|
+
# clover_access_token: result.access_token,
|
|
54
|
+
# clover_refresh_token: result.refresh_token,
|
|
55
|
+
# clover_token_expires_at: result.expires_at
|
|
56
|
+
# )
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
class TokenClient
|
|
61
|
+
# Result object for token operations
|
|
62
|
+
class TokenResult
|
|
63
|
+
attr_reader :access_token, :refresh_token, :expires_at, :expires_in, :error, :raw_response
|
|
64
|
+
|
|
65
|
+
def initialize(success:, access_token: nil, refresh_token: nil, expires_at: nil, expires_in: nil,
|
|
66
|
+
error: nil, raw_response: nil)
|
|
67
|
+
@success = success
|
|
68
|
+
@access_token = access_token
|
|
69
|
+
@refresh_token = refresh_token
|
|
70
|
+
@expires_at = expires_at
|
|
71
|
+
@expires_in = expires_in
|
|
72
|
+
@error = error
|
|
73
|
+
@raw_response = raw_response
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def success?
|
|
77
|
+
@success
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def failure?
|
|
81
|
+
!@success
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# OAuth v2 refresh uses the API domain with /oauth/v2/refresh
|
|
86
|
+
SANDBOX_API_URL = 'https://apisandbox.dev.clover.com'
|
|
87
|
+
PRODUCTION_API_URL = 'https://api.clover.com'
|
|
88
|
+
EU_API_URL = 'https://api.eu.clover.com'
|
|
89
|
+
LA_API_URL = 'https://api.la.clover.com'
|
|
90
|
+
|
|
91
|
+
attr_reader :client_id, :client_secret, :sandbox
|
|
92
|
+
|
|
93
|
+
# Initialize a new TokenClient
|
|
94
|
+
#
|
|
95
|
+
# @param client_id [String] Your Clover App ID
|
|
96
|
+
# @param client_secret [String] Your Clover App Secret
|
|
97
|
+
# @param sandbox [Boolean] Use sandbox environment (default: true)
|
|
98
|
+
def initialize(client_id:, client_secret:, sandbox: true)
|
|
99
|
+
@client_id = client_id
|
|
100
|
+
@client_secret = client_secret
|
|
101
|
+
@sandbox = sandbox
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Refresh an access token using a refresh token
|
|
105
|
+
#
|
|
106
|
+
# @param refresh_token [String] The refresh token to use
|
|
107
|
+
# @return [TokenResult] Result object with new tokens or error
|
|
108
|
+
def refresh_token(refresh_token)
|
|
109
|
+
return TokenResult.new(success: false, error: 'Refresh token is required') if refresh_token.nil? || refresh_token.empty?
|
|
110
|
+
|
|
111
|
+
response = make_refresh_request(refresh_token)
|
|
112
|
+
|
|
113
|
+
if response.success?
|
|
114
|
+
parse_success_response(response)
|
|
115
|
+
else
|
|
116
|
+
parse_error_response(response)
|
|
117
|
+
end
|
|
118
|
+
rescue Faraday::Error => e
|
|
119
|
+
TokenResult.new(success: false, error: "Network error: #{e.message}")
|
|
120
|
+
rescue JSON::ParserError => e
|
|
121
|
+
TokenResult.new(success: false, error: "Invalid JSON response: #{e.message}")
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
TokenResult.new(success: false, error: "Unexpected error: #{e.message}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if a token is expired or about to expire
|
|
127
|
+
#
|
|
128
|
+
# @param expires_at [Time, Integer] Token expiration time
|
|
129
|
+
# @param buffer_seconds [Integer] Buffer before expiration (default: 300 = 5 minutes)
|
|
130
|
+
# @return [Boolean] True if token is expired or will expire within buffer
|
|
131
|
+
def token_expired?(expires_at, buffer_seconds: 300)
|
|
132
|
+
return true if expires_at.nil?
|
|
133
|
+
|
|
134
|
+
expires_at_time = expires_at.is_a?(Integer) ? Time.at(expires_at) : expires_at
|
|
135
|
+
Time.now >= (expires_at_time - buffer_seconds)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Get the API base URL based on sandbox setting
|
|
139
|
+
#
|
|
140
|
+
# @return [String] The API base URL
|
|
141
|
+
def api_base_url
|
|
142
|
+
sandbox ? SANDBOX_API_URL : PRODUCTION_API_URL
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def make_refresh_request(refresh_token)
|
|
148
|
+
# Clover OAuth v2 refresh endpoint uses JSON body
|
|
149
|
+
# Endpoint: /oauth/v2/refresh
|
|
150
|
+
# Required params: client_id, refresh_token
|
|
151
|
+
Faraday.post(refresh_url) do |req|
|
|
152
|
+
req.headers['Content-Type'] = 'application/json'
|
|
153
|
+
req.headers['Accept'] = 'application/json'
|
|
154
|
+
req.body = {
|
|
155
|
+
client_id: client_id,
|
|
156
|
+
refresh_token: refresh_token
|
|
157
|
+
}.to_json
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def refresh_url
|
|
162
|
+
# OAuth v2 uses /oauth/v2/refresh endpoint
|
|
163
|
+
"#{api_base_url}/oauth/v2/refresh"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def parse_success_response(response)
|
|
167
|
+
data = JSON.parse(response.body)
|
|
168
|
+
|
|
169
|
+
# Clover OAuth v2 returns 'access_token_expiration' as Unix timestamp
|
|
170
|
+
# It may also return 'expires_in' as seconds in some contexts
|
|
171
|
+
raw_expiration = data['access_token_expiration'] || data['expires_in']
|
|
172
|
+
|
|
173
|
+
# Determine if it's a timestamp (large number) or seconds (small number)
|
|
174
|
+
# Unix timestamps for 2024+ are > 1700000000
|
|
175
|
+
if raw_expiration && raw_expiration.to_i > 1_600_000_000
|
|
176
|
+
# It's a Unix timestamp
|
|
177
|
+
expires_at = raw_expiration.to_i
|
|
178
|
+
expires_in = expires_at - Time.now.to_i
|
|
179
|
+
elsif raw_expiration
|
|
180
|
+
# It's seconds until expiry
|
|
181
|
+
expires_in = raw_expiration.to_i
|
|
182
|
+
expires_at = Time.now.to_i + expires_in
|
|
183
|
+
else
|
|
184
|
+
expires_in = nil
|
|
185
|
+
expires_at = nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
TokenResult.new(
|
|
189
|
+
success: true,
|
|
190
|
+
access_token: data['access_token'],
|
|
191
|
+
refresh_token: data['refresh_token'],
|
|
192
|
+
expires_in: expires_in,
|
|
193
|
+
expires_at: expires_at,
|
|
194
|
+
raw_response: data
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_error_response(response)
|
|
199
|
+
error_data = begin
|
|
200
|
+
JSON.parse(response.body)
|
|
201
|
+
rescue JSON::ParserError
|
|
202
|
+
{ 'message' => response.body }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
error_message = error_data['message'] || error_data['error'] || "HTTP #{response.status}"
|
|
206
|
+
|
|
207
|
+
TokenResult.new(
|
|
208
|
+
success: false,
|
|
209
|
+
error: error_message,
|
|
210
|
+
raw_response: error_data
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omniauth-clover-oauth2
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dan1d
|
|
@@ -181,6 +181,7 @@ files:
|
|
|
181
181
|
- LICENSE.txt
|
|
182
182
|
- README.md
|
|
183
183
|
- lib/omniauth-clover-oauth2.rb
|
|
184
|
+
- lib/omniauth/clover_oauth2/token_client.rb
|
|
184
185
|
- lib/omniauth/clover_oauth2/version.rb
|
|
185
186
|
- lib/omniauth/strategies/clover_oauth2.rb
|
|
186
187
|
homepage: https://github.com/dan1d/omniauth-clover-oauth2
|