wowsql-sdk 1.3.0 → 3.0.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.
data/lib/wowsql/auth.rb CHANGED
@@ -3,23 +3,112 @@ require 'json'
3
3
  require_relative 'exceptions'
4
4
 
5
5
  module WOWSQL
6
+ # Represents an authenticated user.
7
+ AuthUser = Struct.new(
8
+ :id, :email, :full_name, :avatar_url, :email_verified,
9
+ :user_metadata, :app_metadata, :created_at,
10
+ keyword_init: true
11
+ ) do
12
+ def initialize(id: '', email: '', full_name: nil, avatar_url: nil,
13
+ email_verified: false, user_metadata: nil,
14
+ app_metadata: nil, created_at: nil)
15
+ super
16
+ end
17
+ end
18
+
19
+ # Session tokens returned by the auth service.
20
+ AuthSession = Struct.new(
21
+ :access_token, :refresh_token, :token_type, :expires_in,
22
+ keyword_init: true
23
+ ) do
24
+ def initialize(access_token: '', refresh_token: '', token_type: 'bearer', expires_in: 0)
25
+ super
26
+ end
27
+ end
28
+
29
+ # Response for signup/login requests.
30
+ AuthResponse = Struct.new(:session, :user, keyword_init: true) do
31
+ def initialize(session:, user: nil)
32
+ super
33
+ end
34
+ end
35
+
36
+ # Interface for persisting tokens.
37
+ module TokenStorage
38
+ def get_access_token
39
+ raise NotImplementedError
40
+ end
41
+
42
+ def set_access_token(_token)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ def get_refresh_token
47
+ raise NotImplementedError
48
+ end
49
+
50
+ def set_refresh_token(_token)
51
+ raise NotImplementedError
52
+ end
53
+ end
54
+
55
+ # Default in-memory token storage.
56
+ class MemoryTokenStorage
57
+ include TokenStorage
58
+
59
+ def initialize
60
+ @access = nil
61
+ @refresh = nil
62
+ end
63
+
64
+ def get_access_token
65
+ @access
66
+ end
67
+
68
+ def set_access_token(token)
69
+ @access = token
70
+ end
71
+
72
+ def get_refresh_token
73
+ @refresh
74
+ end
75
+
76
+ def set_refresh_token(token)
77
+ @refresh = token
78
+ end
79
+ end
80
+
6
81
  # Project-level authentication client.
7
- #
82
+ #
8
83
  # UNIFIED AUTHENTICATION: Uses the same API keys (anon/service) as database operations.
9
84
  # One project = one set of keys for ALL operations (auth + database).
10
- #
85
+ #
11
86
  # Key Types:
12
- # - Anonymous Key (wowsql_anon_...): For client-side auth operations (signup, login, OAuth)
13
- # - Service Role Key (wowsql_service_...): For server-side auth operations (admin, full access)
87
+ # - Anonymous Key (wowsql_anon_...): For client-side auth operations
88
+ # - Service Role Key (wowsql_service_...): For server-side auth operations
14
89
  class ProjectAuthClient
15
- def initialize(project_url, api_key, timeout = 30)
90
+ attr_reader :base_url
91
+
92
+ # @param project_url [String] Project subdomain or full URL
93
+ # @param api_key [String] API key for authentication
94
+ # @param base_domain [String] Base domain (default: "wowsql.com")
95
+ # @param secure [Boolean] Use HTTPS (default: true)
96
+ # @param timeout [Integer] Request timeout in seconds (default: 30)
97
+ # @param verify_ssl [Boolean] Verify SSL certificates (default: true)
98
+ # @param token_storage [TokenStorage, nil] Optional token storage
99
+ def initialize(project_url, api_key, base_domain: 'wowsql.com', secure: true,
100
+ timeout: 30, verify_ssl: true, token_storage: nil)
16
101
  @api_key = api_key
17
102
  @timeout = timeout
18
- @base_url = build_auth_base_url(project_url)
19
- @access_token = nil
20
- @refresh_token = nil
103
+ @base_url = build_auth_base_url(project_url, base_domain, secure)
104
+
105
+ @storage = token_storage || MemoryTokenStorage.new
106
+ @access_token = @storage.get_access_token
107
+ @refresh_token = @storage.get_refresh_token
108
+
109
+ ssl_options = verify_ssl ? {} : { verify: false }
21
110
 
22
- @conn = Faraday.new(url: @base_url) do |f|
111
+ @conn = Faraday.new(url: @base_url, ssl: ssl_options) do |f|
23
112
  f.request :json
24
113
  f.response :json
25
114
  f.adapter Faraday.default_adapter
@@ -31,81 +120,79 @@ module WOWSQL
31
120
  end
32
121
 
33
122
  # Sign up a new user.
34
- #
123
+ #
35
124
  # @param email [String] User email
36
125
  # @param password [String] User password (minimum 8 characters)
37
126
  # @param full_name [String, nil] Optional full name
38
127
  # @param user_metadata [Hash, nil] Optional user metadata
39
- # @return [Hash] Response with session and user data
128
+ # @return [AuthResponse]
40
129
  def sign_up(email:, password:, full_name: nil, user_metadata: nil)
41
- payload = {
42
- email: email,
43
- password: password
44
- }
130
+ payload = { email: email, password: password }
45
131
  payload[:full_name] = full_name if full_name
46
132
  payload[:user_metadata] = user_metadata if user_metadata
47
133
 
48
134
  data = request('POST', '/signup', nil, payload)
49
135
  session = persist_session(data)
50
-
51
- {
52
- session: session,
53
- user: data['user'] ? normalize_user(data['user']) : nil
54
- }
136
+ user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
137
+ AuthResponse.new(session: session, user: user)
55
138
  end
56
139
 
57
140
  # Sign in an existing user.
58
- #
141
+ #
59
142
  # @param email [String] User email
60
143
  # @param password [String] User password
61
- # @return [Hash] Response with session and user data
144
+ # @return [AuthResponse]
62
145
  def sign_in(email:, password:)
63
- payload = {
64
- email: email,
65
- password: password
66
- }
67
-
146
+ payload = { email: email, password: password }
68
147
  data = request('POST', '/login', nil, payload)
69
148
  session = persist_session(data)
149
+ AuthResponse.new(session: session, user: nil)
150
+ end
70
151
 
71
- {
72
- session: session,
73
- user: data['user'] ? normalize_user(data['user']) : nil
74
- }
152
+ # Get current authenticated user.
153
+ #
154
+ # @param access_token [String, nil] Override access token
155
+ # @return [AuthUser]
156
+ def get_user(access_token: nil)
157
+ token = access_token || @access_token || @storage.get_access_token
158
+ raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
159
+
160
+ data = request_with_headers('GET', '/me', nil, nil, 'Authorization' => "Bearer #{token}")
161
+ AuthUser.new(**normalize_user(data))
75
162
  end
76
163
 
77
164
  # Get OAuth authorization URL.
78
- #
165
+ #
79
166
  # @param provider [String] OAuth provider name (e.g., 'github', 'google')
80
167
  # @param redirect_uri [String, nil] Optional frontend redirect URI
81
- # @return [Hash] Response with authorization_url and other OAuth details
168
+ # @return [Hash]
82
169
  def get_oauth_authorization_url(provider:, redirect_uri: nil)
83
- raise WOWSQLException.new('provider is required and cannot be empty') if provider.nil? || provider.strip.empty?
170
+ raise WOWSQLError.new('provider is required and cannot be empty') if provider.nil? || provider.strip.empty?
84
171
 
172
+ provider = provider.strip
85
173
  params = {}
86
174
  params['frontend_redirect_uri'] = redirect_uri.strip if redirect_uri
87
175
 
88
176
  begin
89
177
  data = request('GET', "/oauth/#{provider}", params, nil)
90
-
91
178
  {
92
- authorization_url: data['authorization_url'] || '',
93
- provider: data['provider'] || provider,
94
- backend_callback_url: data['backend_callback_url'] || '',
95
- frontend_redirect_uri: data['frontend_redirect_uri'] || redirect_uri || ''
179
+ 'authorization_url' => data['authorization_url'] || '',
180
+ 'provider' => data['provider'] || provider,
181
+ 'backend_callback_url' => data['backend_callback_url'] || '',
182
+ 'frontend_redirect_uri' => data['frontend_redirect_uri'] || redirect_uri || ''
96
183
  }
97
- rescue WOWSQLException => e
184
+ rescue WOWSQLError => e
98
185
  if e.status_code == 502
99
- raise WOWSQLException.new(
100
- "Bad Gateway (502): The backend server may be down or unreachable. Check if the backend is running and accessible at #{@base_url}",
101
- 502,
102
- e.response
186
+ raise WOWSQLError.new(
187
+ "Bad Gateway (502): The backend server may be down or unreachable. " \
188
+ "Check if the backend is running and accessible at #{@base_url}",
189
+ 502, e.response
103
190
  )
104
191
  elsif e.status_code == 400
105
- raise WOWSQLException.new(
106
- "Bad Request (400): #{e.message}. Ensure OAuth provider '#{provider}' is configured and enabled for this project.",
107
- 400,
108
- e.response
192
+ raise WOWSQLError.new(
193
+ "Bad Request (400): #{e.message}. " \
194
+ "Ensure OAuth provider '#{provider}' is configured and enabled for this project.",
195
+ 400, e.response
109
196
  )
110
197
  end
111
198
  raise
@@ -113,229 +200,274 @@ module WOWSQL
113
200
  end
114
201
 
115
202
  # Exchange OAuth callback code for access tokens.
116
- #
203
+ #
117
204
  # @param provider [String] OAuth provider name
118
205
  # @param code [String] Authorization code from OAuth provider callback
119
206
  # @param redirect_uri [String, nil] Optional redirect URI
120
- # @return [Hash] Response with session and user data
207
+ # @return [AuthResponse]
121
208
  def exchange_oauth_callback(provider:, code:, redirect_uri: nil)
122
209
  payload = { code: code }
123
210
  payload[:redirect_uri] = redirect_uri if redirect_uri
124
211
 
125
212
  data = request('POST', "/oauth/#{provider}/callback", nil, payload)
126
213
  session = persist_session(data)
127
-
128
- {
129
- session: session,
130
- user: data['user'] ? normalize_user(data['user']) : nil
131
- }
214
+ user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
215
+ AuthResponse.new(session: session, user: user)
132
216
  end
133
217
 
134
218
  # Request password reset.
135
- #
219
+ #
136
220
  # @param email [String] User's email address
137
- # @return [Hash] Response with success status and message
221
+ # @return [Hash]
138
222
  def forgot_password(email:)
139
223
  payload = { email: email }
140
224
  data = request('POST', '/forgot-password', nil, payload)
141
-
142
225
  {
143
- success: data['success'] != false,
144
- message: data['message'] || 'If that email exists, a password reset link has been sent'
226
+ 'success' => data['success'] != false,
227
+ 'message' => data['message'] || 'If that email exists, a password reset link has been sent'
145
228
  }
146
229
  end
147
230
 
148
231
  # Reset password with token.
149
- #
232
+ #
150
233
  # @param token [String] Password reset token from email
151
234
  # @param new_password [String] New password (minimum 8 characters)
152
- # @return [Hash] Response with success status and message
235
+ # @return [Hash]
153
236
  def reset_password(token:, new_password:)
154
- payload = {
155
- token: token,
156
- new_password: new_password
157
- }
158
-
237
+ payload = { token: token, new_password: new_password }
159
238
  data = request('POST', '/reset-password', nil, payload)
160
-
161
239
  {
162
- success: data['success'] != false,
163
- message: data['message'] || 'Password reset successfully! You can now login with your new password'
240
+ 'success' => data['success'] != false,
241
+ 'message' => data['message'] || 'Password reset successfully! You can now login with your new password'
164
242
  }
165
243
  end
166
244
 
167
245
  # Send OTP code to user's email.
168
- #
246
+ #
169
247
  # @param email [String] User's email address
170
- # @param purpose [String] Purpose of OTP - 'login', 'signup', or 'password_reset' (default: 'login')
171
- # @return [Hash] Response with success status and message
248
+ # @param purpose [String] 'login', 'signup', or 'password_reset'
249
+ # @return [Hash]
172
250
  def send_otp(email:, purpose: 'login')
173
- unless ['login', 'signup', 'password_reset'].include?(purpose)
174
- raise WOWSQLException.new("Purpose must be 'login', 'signup', or 'password_reset'")
251
+ unless %w[login signup password_reset].include?(purpose)
252
+ raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
175
253
  end
176
254
 
177
- payload = {
178
- email: email,
179
- purpose: purpose
180
- }
181
-
255
+ payload = { email: email, purpose: purpose }
182
256
  data = request('POST', '/otp/send', nil, payload)
183
-
184
257
  {
185
- success: data['success'] != false,
186
- message: data['message'] || 'If that email exists, an OTP code has been sent'
258
+ 'success' => data['success'] != false,
259
+ 'message' => data['message'] || 'If that email exists, an OTP code has been sent'
187
260
  }
188
261
  end
189
262
 
190
263
  # Verify OTP and complete authentication.
191
- #
264
+ #
192
265
  # @param email [String] User's email address
193
266
  # @param otp [String] 6-digit OTP code
194
- # @param purpose [String] Purpose of OTP - 'login', 'signup', or 'password_reset' (default: 'login')
195
- # @param new_password [String, nil] Required for password_reset purpose, new password (minimum 8 characters)
196
- # @return [Hash] Response with session and user data (for login/signup) or success message (for password_reset)
267
+ # @param purpose [String] 'login', 'signup', or 'password_reset'
268
+ # @param new_password [String, nil] Required for password_reset purpose
269
+ # @return [AuthResponse, Hash]
197
270
  def verify_otp(email:, otp:, purpose: 'login', new_password: nil)
198
- unless ['login', 'signup', 'password_reset'].include?(purpose)
199
- raise WOWSQLException.new("Purpose must be 'login', 'signup', or 'password_reset'")
271
+ unless %w[login signup password_reset].include?(purpose)
272
+ raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
200
273
  end
201
274
 
202
275
  if purpose == 'password_reset' && new_password.nil?
203
- raise WOWSQLException.new('new_password is required for password_reset purpose')
276
+ raise WOWSQLError.new('new_password is required for password_reset purpose')
204
277
  end
205
278
 
206
- payload = {
207
- email: email,
208
- otp: otp,
209
- purpose: purpose
210
- }
279
+ payload = { email: email, otp: otp, purpose: purpose }
211
280
  payload[:new_password] = new_password if new_password
212
281
 
213
282
  data = request('POST', '/otp/verify', nil, payload)
214
283
 
215
284
  if purpose == 'password_reset'
216
285
  return {
217
- success: data['success'] != false,
218
- message: data['message'] || 'Password reset successfully! You can now login with your new password'
286
+ 'success' => data['success'] != false,
287
+ 'message' => data['message'] || 'Password reset successfully! You can now login with your new password'
219
288
  }
220
289
  end
221
290
 
222
291
  session = persist_session(data)
223
-
224
- {
225
- session: session,
226
- user: data['user'] ? normalize_user(data['user']) : nil
227
- }
292
+ user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
293
+ AuthResponse.new(session: session, user: user)
228
294
  end
229
295
 
230
296
  # Send magic link to user's email.
231
- #
297
+ #
232
298
  # @param email [String] User's email address
233
- # @param purpose [String] Purpose of magic link - 'login', 'signup', or 'email_verification' (default: 'login')
234
- # @return [Hash] Response with success status and message
299
+ # @param purpose [String] 'login', 'signup', or 'email_verification'
300
+ # @return [Hash]
235
301
  def send_magic_link(email:, purpose: 'login')
236
- unless ['login', 'signup', 'email_verification'].include?(purpose)
237
- raise WOWSQLException.new("Purpose must be 'login', 'signup', or 'email_verification'")
302
+ unless %w[login signup email_verification].include?(purpose)
303
+ raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'email_verification'")
238
304
  end
239
305
 
240
- payload = {
241
- email: email,
242
- purpose: purpose
243
- }
244
-
306
+ payload = { email: email, purpose: purpose }
245
307
  data = request('POST', '/magic-link/send', nil, payload)
246
-
247
308
  {
248
- success: data['success'] != false,
249
- message: data['message'] || 'If that email exists, a magic link has been sent'
309
+ 'success' => data['success'] != false,
310
+ 'message' => data['message'] || 'If that email exists, a magic link has been sent'
250
311
  }
251
312
  end
252
313
 
253
314
  # Verify email using token.
254
- #
315
+ #
255
316
  # @param token [String] Verification token from email
256
- # @return [Hash] Response with success status, message, and user info
317
+ # @return [Hash]
257
318
  def verify_email(token:)
258
319
  payload = { token: token }
259
320
  data = request('POST', '/verify-email', nil, payload)
260
-
261
321
  {
262
- success: data['success'] != false,
263
- message: data['message'] || 'Email verified successfully!',
264
- user: data['user'] ? normalize_user(data['user']) : nil
322
+ 'success' => data['success'] != false,
323
+ 'message' => data['message'] || 'Email verified successfully!',
324
+ 'user' => data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
265
325
  }
266
326
  end
267
327
 
268
328
  # Resend verification email.
269
- #
329
+ #
270
330
  # @param email [String] User's email address
271
- # @return [Hash] Response with success status and message
331
+ # @return [Hash]
272
332
  def resend_verification(email:)
273
333
  payload = { email: email }
274
334
  data = request('POST', '/resend-verification', nil, payload)
275
-
276
335
  {
277
- success: data['success'] != false,
278
- message: data['message'] || 'If that email exists, a verification email has been sent'
336
+ 'success' => data['success'] != false,
337
+ 'message' => data['message'] || 'If that email exists, a verification email has been sent'
279
338
  }
280
339
  end
281
340
 
341
+ # Logout the current user by invalidating their session.
342
+ #
343
+ # @param access_token [String, nil] Override access token
344
+ # @return [Hash]
345
+ def logout(access_token: nil)
346
+ token = access_token || @access_token || @storage.get_access_token
347
+ raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
348
+
349
+ data = request_with_headers('POST', '/logout', nil, nil, 'Authorization' => "Bearer #{token}")
350
+ clear_session
351
+ data
352
+ end
353
+
354
+ # Exchange a refresh token for new access + refresh tokens.
355
+ #
356
+ # @param refresh_token [String, nil] Override refresh token
357
+ # @return [AuthResponse]
358
+ def refresh_token(refresh_token: nil)
359
+ token = refresh_token || @refresh_token || @storage.get_refresh_token
360
+ raise WOWSQLError.new('Refresh token is required. Call sign_in first.') unless token
361
+
362
+ data = request('POST', '/refresh-token', nil, { refresh_token: token })
363
+ session = persist_session(data)
364
+ AuthResponse.new(session: session, user: nil)
365
+ end
366
+
367
+ # Change the authenticated user's password.
368
+ #
369
+ # @param current_password [String] Current password
370
+ # @param new_password [String] New password (minimum 8 characters)
371
+ # @param access_token [String, nil] Override access token
372
+ # @return [Hash]
373
+ def change_password(current_password:, new_password:, access_token: nil)
374
+ token = access_token || @access_token || @storage.get_access_token
375
+ raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
376
+
377
+ request_with_headers(
378
+ 'POST', '/change-password', nil,
379
+ { current_password: current_password, new_password: new_password },
380
+ 'Authorization' => "Bearer #{token}"
381
+ )
382
+ end
383
+
384
+ # Update the authenticated user's profile.
385
+ #
386
+ # @param full_name [String, nil] Updated full name
387
+ # @param avatar_url [String, nil] Updated avatar URL
388
+ # @param username [String, nil] Updated username
389
+ # @param user_metadata [Hash, nil] Updated metadata
390
+ # @param access_token [String, nil] Override access token
391
+ # @return [AuthUser]
392
+ def update_user(full_name: nil, avatar_url: nil, username: nil, user_metadata: nil, access_token: nil)
393
+ token = access_token || @access_token || @storage.get_access_token
394
+ raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token
395
+
396
+ payload = {}
397
+ payload[:full_name] = full_name unless full_name.nil?
398
+ payload[:avatar_url] = avatar_url unless avatar_url.nil?
399
+ payload[:username] = username unless username.nil?
400
+ payload[:user_metadata] = user_metadata unless user_metadata.nil?
401
+ raise WOWSQLError.new('At least one field to update is required') if payload.empty?
402
+
403
+ data = request_with_headers('PATCH', '/me', nil, payload, 'Authorization' => "Bearer #{token}")
404
+ AuthUser.new(**normalize_user(data))
405
+ end
406
+
282
407
  # Get current session tokens.
283
- #
284
- # @return [Hash] Current access_token and refresh_token
408
+ #
409
+ # @return [Hash]
285
410
  def get_session
286
411
  {
287
- access_token: @access_token,
288
- refresh_token: @refresh_token
412
+ 'access_token' => @access_token || @storage.get_access_token,
413
+ 'refresh_token' => @refresh_token || @storage.get_refresh_token
289
414
  }
290
415
  end
291
416
 
292
417
  # Set session tokens.
293
- #
418
+ #
294
419
  # @param access_token [String] Access token
295
420
  # @param refresh_token [String, nil] Optional refresh token
296
421
  def set_session(access_token:, refresh_token: nil)
297
422
  @access_token = access_token
298
423
  @refresh_token = refresh_token
424
+ @storage.set_access_token(access_token)
425
+ @storage.set_refresh_token(refresh_token)
299
426
  end
300
427
 
301
428
  # Clear session tokens.
302
429
  def clear_session
303
430
  @access_token = nil
304
431
  @refresh_token = nil
432
+ @storage.set_access_token(nil)
433
+ @storage.set_refresh_token(nil)
434
+ end
435
+
436
+ # Close the HTTP connection.
437
+ def close
438
+ @conn.close if @conn.respond_to?(:close)
305
439
  end
306
440
 
307
441
  private
308
442
 
309
- # Build authentication base URL from project URL.
310
- def build_auth_base_url(project_url)
443
+ def build_auth_base_url(project_url, base_domain, secure)
311
444
  normalized = project_url.strip
312
445
 
313
- # If it's already a full URL, use it as-is
314
446
  if normalized.start_with?('http://') || normalized.start_with?('https://')
315
447
  normalized = normalized.chomp('/')
316
- normalized = normalized[0..-5] if normalized.end_with?('/api')
448
+ normalized = normalized[0...-4] if normalized.end_with?('/api')
317
449
  return "#{normalized}/api/auth"
318
450
  end
319
451
 
320
- # If it already contains the base domain, don't append it again
321
- if normalized.include?('.wowsql.com') || normalized.end_with?('wowsql.com')
322
- normalized = "https://#{normalized}"
452
+ protocol = secure ? 'https' : 'http'
453
+ if normalized.include?(".#{base_domain}") || normalized.end_with?(base_domain)
454
+ normalized = "#{protocol}://#{normalized}"
323
455
  else
324
- # Just a project slug, append domain
325
- normalized = "https://#{normalized}.wowsql.com"
456
+ normalized = "#{protocol}://#{normalized}.#{base_domain}"
326
457
  end
327
458
 
328
459
  normalized = normalized.chomp('/')
329
- normalized = normalized[0..-5] if normalized.end_with?('/api')
460
+ normalized = normalized[0...-4] if normalized.end_with?('/api')
330
461
 
331
462
  "#{normalized}/api/auth"
332
463
  end
333
464
 
334
- # Normalize user data from API response.
335
465
  def normalize_user(user)
466
+ return { id: '', email: '' } unless user
467
+
336
468
  {
337
- id: user['id'] || user[:id],
338
- email: user['email'] || user[:email],
469
+ id: user['id'] || user[:id] || '',
470
+ email: user['email'] || user[:email] || '',
339
471
  full_name: user['full_name'] || user['fullName'] || user[:full_name] || user[:fullName],
340
472
  avatar_url: user['avatar_url'] || user['avatarUrl'] || user[:avatar_url] || user[:avatarUrl],
341
473
  email_verified: !!(user['email_verified'] || user['emailVerified'] || user[:email_verified] || user[:emailVerified]),
@@ -345,40 +477,57 @@ module WOWSQL
345
477
  }
346
478
  end
347
479
 
348
- # Persist session tokens from API response.
349
480
  def persist_session(data)
350
- session = {
481
+ session = AuthSession.new(
351
482
  access_token: data['access_token'] || data[:access_token] || '',
352
483
  refresh_token: data['refresh_token'] || data[:refresh_token] || '',
353
484
  token_type: data['token_type'] || data[:token_type] || 'bearer',
354
485
  expires_in: data['expires_in'] || data[:expires_in] || 0
355
- }
486
+ )
356
487
 
357
- @access_token = session[:access_token]
358
- @refresh_token = session[:refresh_token]
488
+ @access_token = session.access_token
489
+ @refresh_token = session.refresh_token
490
+ @storage.set_access_token(session.access_token)
491
+ @storage.set_refresh_token(session.refresh_token)
359
492
 
360
493
  session
361
494
  end
362
495
 
363
- # Make HTTP request to API.
364
496
  def request(method, path, params = nil, json = nil)
365
- begin
366
- response = @conn.public_send(method.downcase, path) do |req|
367
- req.params = params if params
368
- req.body = json if json
369
- end
497
+ response = @conn.public_send(method.downcase, path) do |req|
498
+ req.params = params if params
499
+ req.body = json if json
500
+ end
370
501
 
371
- if response.status >= 400
372
- error_data = response.body.is_a?(Hash) ? response.body : {}
373
- error_msg = error_data['detail'] || error_data['message'] || error_data['error'] || "Request failed with status #{response.status}"
374
- raise WOWSQLException.new(error_msg, response.status, error_data)
375
- end
502
+ if response.status >= 400
503
+ error_data = response.body.is_a?(Hash) ? response.body : {}
504
+ error_msg = error_data['detail'] || error_data['message'] || error_data['error'] ||
505
+ "Request failed with status #{response.status}"
506
+ raise WOWSQLError.new(error_msg, response.status, error_data)
507
+ end
376
508
 
377
- response.body || {}
378
- rescue Faraday::Error => e
379
- raise WOWSQLException.new("Request failed: #{e.message}")
509
+ response.body || {}
510
+ rescue Faraday::Error => e
511
+ raise WOWSQLError.new("Request failed: #{e.message}")
512
+ end
513
+
514
+ def request_with_headers(method, path, params, json, extra_headers)
515
+ response = @conn.public_send(method.downcase, path) do |req|
516
+ req.params = params if params
517
+ req.body = json if json
518
+ extra_headers.each { |k, v| req.headers[k] = v } if extra_headers
519
+ end
520
+
521
+ if response.status >= 400
522
+ error_data = response.body.is_a?(Hash) ? response.body : {}
523
+ error_msg = error_data['detail'] || error_data['message'] || error_data['error'] ||
524
+ "Request failed with status #{response.status}"
525
+ raise WOWSQLError.new(error_msg, response.status, error_data)
380
526
  end
527
+
528
+ response.body || {}
529
+ rescue Faraday::Error => e
530
+ raise WOWSQLError.new("Request failed: #{e.message}")
381
531
  end
382
532
  end
383
533
  end
384
-