Class: CloverSandboxSimulator::Services::Clover::OauthService

Inherits:
BaseService
  • Object
show all
Defined in:
lib/clover_sandbox_simulator/services/clover/oauth_service.rb

Overview

Manages Clover OAuth2 authentication Handles token refresh and validation using omniauth-clover-oauth2

Usage:

oauth = OauthService.new

# Check if token needs refresh
if oauth.token_expired?
  new_token = oauth.refresh_token
end

# Get authorization URL for initial auth
url = oauth.authorization_url

Constant Summary collapse

SANDBOX_AUTH_URL =

Sandbox OAuth endpoints

"https://sandbox.dev.clover.com/oauth/v2/authorize"
SANDBOX_TOKEN_URL =
"https://sandbox.dev.clover.com/oauth/v2/token"
PRODUCTION_AUTH_URL =

Production OAuth endpoints

"https://www.clover.com/oauth/v2/authorize"
PRODUCTION_TOKEN_URL =
"https://www.clover.com/oauth/v2/token"

Instance Attribute Summary collapse

Attributes inherited from BaseService

#config, #logger

Instance Method Summary collapse

Constructor Details

#initialize(config: nil, app_id: nil, app_secret: nil) ⇒ OauthService

Returns a new instance of OauthService.



33
34
35
36
37
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 33

def initialize(config: nil, app_id: nil, app_secret: nil)
  super(config: config)
  @app_id = app_id || ENV.fetch("CLOVER_APP_ID", nil)
  @app_secret = app_secret || ENV.fetch("CLOVER_APP_SECRET", nil)
end

Instance Attribute Details

#app_idObject (readonly)

Returns the value of attribute app_id.



31
32
33
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 31

def app_id
  @app_id
end

#app_secretObject (readonly)

Returns the value of attribute app_secret.



31
32
33
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 31

def app_secret
  @app_secret
end

Instance Method Details

#authorization_url(redirect_uri:, merchant_id: nil) ⇒ String

Get the authorization URL for user consent

Parameters:

  • redirect_uri (String)

    Callback URL after authorization

  • merchant_id (String, nil) (defaults to: nil)

    Optional merchant ID to pre-select

Returns:

  • (String)

    Authorization URL



50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 50

def authorization_url(redirect_uri:, merchant_id: nil)
  validate_oauth_config!

  params = {
    client_id: app_id,
    redirect_uri: redirect_uri,
    response_type: "code"
  }
  params[:merchant_id] = merchant_id if merchant_id

  "#{auth_endpoint}?#{URI.encode_www_form(params)}"
end

#decode_token(token) ⇒ Hash

Get token info from a JWT

Parameters:

  • token (String)

    JWT token to decode

Returns:

  • (Hash)

    Decoded payload



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 148

def decode_token(token)
  parts = token.split(".")
  raise ArgumentError, "Invalid JWT format" unless parts.length == 3

  payload = JSON.parse(Base64.decode64(parts[1]))

  {
    merchant_id: payload["merchant_uuid"],
    app_id: payload["app_uuid"],
    issued_at: Time.at(payload["iat"]),
    expires_at: Time.at(payload["exp"]),
    permissions: payload["permission_bitmap"]
  }
rescue StandardError => e
  logger.error "Failed to decode token: #{e.message}"
  {}
end

#exchange_code(code:, redirect_uri:) ⇒ Hash

Exchange authorization code for tokens

Parameters:

  • code (String)

    Authorization code from callback

  • redirect_uri (String)

    Same redirect URI used in authorization

Returns:

  • (Hash)

    Token response with access_token, refresh_token, etc.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 68

def exchange_code(code:, redirect_uri:)
  validate_oauth_config!

  logger.info "Exchanging authorization code for tokens..."

  response = RestClient.post(
    token_endpoint,
    {
      client_id: app_id,
      client_secret: app_secret,
      code: code,
      redirect_uri: redirect_uri,
      grant_type: "authorization_code"
    },
    { "Content-Type" => "application/x-www-form-urlencoded" }
  )

  tokens = JSON.parse(response.body)
  logger.info "Token exchange successful"
  tokens
rescue RestClient::ExceptionWithResponse => e
  handle_oauth_error(e)
end

#oauth_configured?Boolean

Check if OAuth credentials are configured

Returns:

  • (Boolean)


40
41
42
43
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 40

def oauth_configured?
  !app_id.nil? && !app_id.empty? &&
    !app_secret.nil? && !app_secret.empty?
end

#refresh_current_merchant_tokenHash?

Refresh the token for the current merchant using stored refresh token Updates both config and .env.json

Returns:

  • (Hash, nil)

    New token response or nil on failure



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 178

def refresh_current_merchant_token
  unless config.refresh_token && !config.refresh_token.empty?
    logger.warn "No refresh token available for current merchant"
    return nil
  end

  tokens = refresh_token(config.refresh_token)
  return nil unless tokens

  # Update config
  update_config_token(tokens["access_token"])

  # Save to .env.json
  save_tokens_to_json(
    config.merchant_id,
    access_token: tokens["access_token"],
    refresh_token: tokens["refresh_token"]
  )

  tokens
end

#refresh_token(refresh_token) ⇒ Hash

Refresh an expired access token

Parameters:

  • refresh_token (String)

    The refresh token

Returns:

  • (Hash)

    New token response



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 96

def refresh_token(refresh_token)
  validate_oauth_config!

  logger.info "Refreshing access token..."

  response = RestClient.post(
    token_endpoint,
    {
      client_id: app_id,
      client_secret: app_secret,
      refresh_token: refresh_token,
      grant_type: "refresh_token"
    },
    { "Content-Type" => "application/x-www-form-urlencoded" }
  )

  tokens = JSON.parse(response.body)
  logger.info "Token refresh successful"
  tokens
rescue RestClient::ExceptionWithResponse => e
  handle_oauth_error(e)
end

#save_token_to_json(merchant_id, access_token) ⇒ Object

Alias for backwards compatibility



228
229
230
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 228

def save_token_to_json(merchant_id, access_token)
  save_tokens_to_json(merchant_id, access_token: access_token)
end

#save_tokens_to_json(merchant_id, access_token: nil, refresh_token: nil) ⇒ Object

Save tokens to .env.json for a specific merchant

Parameters:

  • merchant_id (String)

    Merchant ID to update

  • access_token (String, nil) (defaults to: nil)

    New access token

  • refresh_token (String, nil) (defaults to: nil)

    New refresh token



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 205

def save_tokens_to_json(merchant_id, access_token: nil, refresh_token: nil)
  file_path = config.class::MERCHANTS_FILE
  return unless File.exist?(file_path)

  merchants = JSON.parse(File.read(file_path))

  merchant = merchants.find { |m| m["CLOVER_MERCHANT_ID"] == merchant_id }
  if merchant
    merchant["CLOVER_API_TOKEN"] = access_token if access_token
    merchant["CLOVER_REFRESH_TOKEN"] = refresh_token if refresh_token
    # Also update config
    config.refresh_token = refresh_token if refresh_token

    File.write(file_path, JSON.pretty_generate(merchants))
    logger.info "Tokens saved to .env.json for merchant #{merchant_id}"
  else
    logger.warn "Merchant #{merchant_id} not found in .env.json"
  end
rescue StandardError => e
  logger.error "Failed to save tokens: #{e.message}"
end

#token_expired?(token = nil) ⇒ Boolean

Check if the current API token is expired Clover JWT tokens have an ‘exp’ claim

Parameters:

  • token (String, nil) (defaults to: nil)

    Token to check (defaults to config.api_token)

Returns:

  • (Boolean)

    True if token is expired or invalid



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 124

def token_expired?(token = nil)
  token ||= config.api_token
  return true if token.nil? || token.empty? || token == "NEEDS_REFRESH"

  # Decode JWT to check expiration
  parts = token.split(".")
  return true unless parts.length == 3

  payload = JSON.parse(Base64.decode64(parts[1]))
  exp = payload["exp"]

  return true unless exp

  # Token is expired if exp is in the past (with 60s buffer)
  Time.now.to_i >= (exp - 60)
rescue StandardError => e
  logger.debug "Could not decode token: #{e.message}"
  true
end

#update_config_token(access_token) ⇒ Object

Update the configuration with a new token

Parameters:

  • access_token (String)

    New access token



169
170
171
172
# File 'lib/clover_sandbox_simulator/services/clover/oauth_service.rb', line 169

def update_config_token(access_token)
  config.api_token = access_token
  logger.info "Configuration updated with new token"
end