Class: WOWSQL::ProjectAuthClient

Inherits:
Object
  • Object
show all
Defined in:
lib/wowsql/auth.rb

Overview

Project-level authentication client.

UNIFIED AUTHENTICATION: Uses the same API keys (anon/service) as database operations. One project = one set of keys for ALL operations (auth + database).

Key Types:

- Anonymous Key (wowsql_anon_...): For client-side auth operations
- Service Role Key (wowsql_service_...): For server-side auth operations

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project_url, api_key, base_domain: 'wowsqlconnect.com', secure: true, timeout: 30, verify_ssl: true, token_storage: nil) ⇒ ProjectAuthClient

Returns a new instance of ProjectAuthClient.

Parameters:

  • project_url (String)

    Project subdomain or full URL

  • api_key (String)

    API key for authentication

  • base_domain (String) (defaults to: 'wowsqlconnect.com')

    Base domain (default: “wowsqlconnect.com”)

  • secure (Boolean) (defaults to: true)

    Use HTTPS (default: true)

  • timeout (Integer) (defaults to: 30)

    Request timeout in seconds (default: 30)

  • verify_ssl (Boolean) (defaults to: true)

    Verify SSL certificates (default: true)

  • token_storage (TokenStorage, nil) (defaults to: nil)

    Optional token storage



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/wowsql/auth.rb', line 99

def initialize(project_url, api_key, base_domain: 'wowsqlconnect.com', secure: true,
               timeout: 30, verify_ssl: true, token_storage: nil)
  @api_key = api_key
  @timeout = timeout
  @base_url = build_auth_base_url(project_url, base_domain, secure)

  @storage = token_storage || MemoryTokenStorage.new
  @access_token = @storage.get_access_token
  @refresh_token = @storage.get_refresh_token

  ssl_options = verify_ssl ? {} : { verify: false }

  @conn = Faraday.new(url: @base_url, ssl: ssl_options) do |f|
    f.request :json
    f.response :json
    f.adapter Faraday.default_adapter
    f.options.timeout = timeout
  end

  @conn.headers['Authorization'] = "Bearer #{api_key}"
  @conn.headers['Content-Type'] = 'application/json'
end

Instance Attribute Details

#base_urlObject (readonly)

Returns the value of attribute base_url.



90
91
92
# File 'lib/wowsql/auth.rb', line 90

def base_url
  @base_url
end

Instance Method Details

#change_password(current_password:, new_password:, access_token: nil) ⇒ Hash

Change the authenticated user’s password.

Parameters:

  • current_password (String)

    Current password

  • new_password (String)

    New password (minimum 8 characters)

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

    Override access token

Returns:

  • (Hash)

Raises:



373
374
375
376
377
378
379
380
381
382
# File 'lib/wowsql/auth.rb', line 373

def change_password(current_password:, new_password:, access_token: nil)
  token = access_token || @access_token || @storage.get_access_token
  raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token

  request_with_headers(
    'POST', '/change-password', nil,
    { current_password: current_password, new_password: new_password },
    'Authorization' => "Bearer #{token}"
  )
end

#clear_sessionObject

Clear session tokens.



429
430
431
432
433
434
# File 'lib/wowsql/auth.rb', line 429

def clear_session
  @access_token = nil
  @refresh_token = nil
  @storage.set_access_token(nil)
  @storage.set_refresh_token(nil)
end

#closeObject

Close the HTTP connection.



437
438
439
# File 'lib/wowsql/auth.rb', line 437

def close
  @conn.close if @conn.respond_to?(:close)
end

#exchange_oauth_callback(provider:, code:, redirect_uri: nil) ⇒ AuthResponse

Exchange OAuth callback code for access tokens.

Parameters:

  • provider (String)

    OAuth provider name

  • code (String)

    Authorization code from OAuth provider callback

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

    Optional redirect URI

Returns:



208
209
210
211
212
213
214
215
216
# File 'lib/wowsql/auth.rb', line 208

def exchange_oauth_callback(provider:, code:, redirect_uri: nil)
  payload = { code: code }
  payload[:redirect_uri] = redirect_uri if redirect_uri

  data = request('POST', "/oauth/#{provider}/callback", nil, payload)
  session = persist_session(data)
  user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
  AuthResponse.new(session: session, user: user)
end

#forgot_password(email:) ⇒ Hash

Request password reset.

Parameters:

  • email (String)

    User’s email address

Returns:

  • (Hash)


222
223
224
225
226
227
228
229
# File 'lib/wowsql/auth.rb', line 222

def forgot_password(email:)
  payload = { email: email }
  data = request('POST', '/forgot-password', nil, payload)
  {
    'success' => data['success'] != false,
    'message' => data['message'] || 'If that email exists, a password reset link has been sent'
  }
end

#get_oauth_authorization_url(provider:, redirect_uri: nil) ⇒ Hash

Get OAuth authorization URL.

Parameters:

  • provider (String)

    OAuth provider name (e.g., ‘github’, ‘google’)

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

    Optional frontend redirect URI

Returns:

  • (Hash)

Raises:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/wowsql/auth.rb', line 169

def get_oauth_authorization_url(provider:, redirect_uri: nil)
  raise WOWSQLError.new('provider is required and cannot be empty') if provider.nil? || provider.strip.empty?

  provider = provider.strip
  params = {}
  params['frontend_redirect_uri'] = redirect_uri.strip if redirect_uri

  begin
    data = request('GET', "/oauth/#{provider}", params, nil)
    {
      'authorization_url' => data['authorization_url'] || '',
      'provider' => data['provider'] || provider,
      'backend_callback_url' => data['backend_callback_url'] || '',
      'frontend_redirect_uri' => data['frontend_redirect_uri'] || redirect_uri || ''
    }
  rescue WOWSQLError => e
    if e.status_code == 502
      raise WOWSQLError.new(
        "Bad Gateway (502): The backend server may be down or unreachable. " \
        "Check if the backend is running and accessible at #{@base_url}",
        502, e.response
      )
    elsif e.status_code == 400
      raise WOWSQLError.new(
        "Bad Request (400): #{e.message}. " \
        "Ensure OAuth provider '#{provider}' is configured and enabled for this project.",
        400, e.response
      )
    end
    raise
  end
end

#get_sessionHash

Get current session tokens.

Returns:

  • (Hash)


410
411
412
413
414
415
# File 'lib/wowsql/auth.rb', line 410

def get_session
  {
    'access_token' => @access_token || @storage.get_access_token,
    'refresh_token' => @refresh_token || @storage.get_refresh_token
  }
end

#get_user(access_token: nil) ⇒ AuthUser

Get current authenticated user.

Parameters:

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

    Override access token

Returns:

Raises:



156
157
158
159
160
161
162
# File 'lib/wowsql/auth.rb', line 156

def get_user(access_token: nil)
  token = access_token || @access_token || @storage.get_access_token
  raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token

  data = request_with_headers('GET', '/me', nil, nil, 'Authorization' => "Bearer #{token}")
  AuthUser.new(**normalize_user(data))
end

#logout(access_token: nil) ⇒ Hash

Logout the current user by invalidating their session.

Parameters:

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

    Override access token

Returns:

  • (Hash)

Raises:



345
346
347
348
349
350
351
352
# File 'lib/wowsql/auth.rb', line 345

def logout(access_token: nil)
  token = access_token || @access_token || @storage.get_access_token
  raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token

  data = request_with_headers('POST', '/logout', nil, nil, 'Authorization' => "Bearer #{token}")
  clear_session
  data
end

#refresh_token(refresh_token: nil) ⇒ AuthResponse

Exchange a refresh token for new access + refresh tokens.

Parameters:

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

    Override refresh token

Returns:

Raises:



358
359
360
361
362
363
364
365
# File 'lib/wowsql/auth.rb', line 358

def refresh_token(refresh_token: nil)
  token = refresh_token || @refresh_token || @storage.get_refresh_token
  raise WOWSQLError.new('Refresh token is required. Call sign_in first.') unless token

  data = request('POST', '/refresh-token', nil, { refresh_token: token })
  session = persist_session(data)
  AuthResponse.new(session: session, user: nil)
end

#resend_verification(email:) ⇒ Hash

Resend verification email.

Parameters:

  • email (String)

    User’s email address

Returns:

  • (Hash)


332
333
334
335
336
337
338
339
# File 'lib/wowsql/auth.rb', line 332

def resend_verification(email:)
  payload = { email: email }
  data = request('POST', '/resend-verification', nil, payload)
  {
    'success' => data['success'] != false,
    'message' => data['message'] || 'If that email exists, a verification email has been sent'
  }
end

#reset_password(token:, new_password:) ⇒ Hash

Reset password with token.

Parameters:

  • token (String)

    Password reset token from email

  • new_password (String)

    New password (minimum 8 characters)

Returns:

  • (Hash)


236
237
238
239
240
241
242
243
# File 'lib/wowsql/auth.rb', line 236

def reset_password(token:, new_password:)
  payload = { token: token, new_password: new_password }
  data = request('POST', '/reset-password', nil, payload)
  {
    'success' => data['success'] != false,
    'message' => data['message'] || 'Password reset successfully! You can now login with your new password'
  }
end

Send magic link to user’s email.

Parameters:

  • email (String)

    User’s email address

  • purpose (String) (defaults to: 'login')

    ‘login’, ‘signup’, or ‘email_verification’

Returns:

  • (Hash)


301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/wowsql/auth.rb', line 301

def send_magic_link(email:, purpose: 'login')
  unless %w[login signup email_verification].include?(purpose)
    raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'email_verification'")
  end

  payload = { email: email, purpose: purpose }
  data = request('POST', '/magic-link/send', nil, payload)
  {
    'success' => data['success'] != false,
    'message' => data['message'] || 'If that email exists, a magic link has been sent'
  }
end

#send_otp(email:, purpose: 'login') ⇒ Hash

Send OTP code to user’s email.

Parameters:

  • email (String)

    User’s email address

  • purpose (String) (defaults to: 'login')

    ‘login’, ‘signup’, or ‘password_reset’

Returns:

  • (Hash)


250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/wowsql/auth.rb', line 250

def send_otp(email:, purpose: 'login')
  unless %w[login signup password_reset].include?(purpose)
    raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
  end

  payload = { email: email, purpose: purpose }
  data = request('POST', '/otp/send', nil, payload)
  {
    'success' => data['success'] != false,
    'message' => data['message'] || 'If that email exists, an OTP code has been sent'
  }
end

#set_session(access_token:, refresh_token: nil) ⇒ Object

Set session tokens.

Parameters:

  • access_token (String)

    Access token

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

    Optional refresh token



421
422
423
424
425
426
# File 'lib/wowsql/auth.rb', line 421

def set_session(access_token:, refresh_token: nil)
  @access_token = access_token
  @refresh_token = refresh_token
  @storage.set_access_token(access_token)
  @storage.set_refresh_token(refresh_token)
end

#sign_in(email:, password:) ⇒ AuthResponse

Sign in an existing user.

Parameters:

  • email (String)

    User email

  • password (String)

    User password

Returns:



145
146
147
148
149
150
# File 'lib/wowsql/auth.rb', line 145

def (email:, password:)
  payload = { email: email, password: password }
  data = request('POST', '/login', nil, payload)
  session = persist_session(data)
  AuthResponse.new(session: session, user: nil)
end

#sign_up(email:, password:, full_name: nil, user_metadata: nil) ⇒ AuthResponse

Sign up a new user.

Parameters:

  • email (String)

    User email

  • password (String)

    User password (minimum 8 characters)

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

    Optional full name

  • user_metadata (Hash, nil) (defaults to: nil)

    Optional user metadata

Returns:



129
130
131
132
133
134
135
136
137
138
# File 'lib/wowsql/auth.rb', line 129

def (email:, password:, full_name: nil, user_metadata: nil)
  payload = { email: email, password: password }
  payload[:full_name] = full_name if full_name
  payload[:user_metadata] =  if 

  data = request('POST', '/signup', nil, payload)
  session = persist_session(data)
  user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
  AuthResponse.new(session: session, user: user)
end

#update_user(full_name: nil, avatar_url: nil, username: nil, user_metadata: nil, access_token: nil) ⇒ AuthUser

Update the authenticated user’s profile.

Parameters:

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

    Updated full name

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

    Updated avatar URL

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

    Updated username

  • user_metadata (Hash, nil) (defaults to: nil)

    Updated metadata

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

    Override access token

Returns:

Raises:



392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/wowsql/auth.rb', line 392

def update_user(full_name: nil, avatar_url: nil, username: nil, user_metadata: nil, access_token: nil)
  token = access_token || @access_token || @storage.get_access_token
  raise WOWSQLError.new('Access token is required. Call sign_in first.') unless token

  payload = {}
  payload[:full_name] = full_name unless full_name.nil?
  payload[:avatar_url] = avatar_url unless avatar_url.nil?
  payload[:username] = username unless username.nil?
  payload[:user_metadata] =  unless .nil?
  raise WOWSQLError.new('At least one field to update is required') if payload.empty?

  data = request_with_headers('PATCH', '/me', nil, payload, 'Authorization' => "Bearer #{token}")
  AuthUser.new(**normalize_user(data))
end

#verify_email(token:) ⇒ Hash

Verify email using token.

Parameters:

  • token (String)

    Verification token from email

Returns:

  • (Hash)


318
319
320
321
322
323
324
325
326
# File 'lib/wowsql/auth.rb', line 318

def verify_email(token:)
  payload = { token: token }
  data = request('POST', '/verify-email', nil, payload)
  {
    'success' => data['success'] != false,
    'message' => data['message'] || 'Email verified successfully!',
    'user' => data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
  }
end

#verify_otp(email:, otp:, purpose: 'login', new_password: nil) ⇒ AuthResponse, Hash

Verify OTP and complete authentication.

Parameters:

  • email (String)

    User’s email address

  • otp (String)

    6-digit OTP code

  • purpose (String) (defaults to: 'login')

    ‘login’, ‘signup’, or ‘password_reset’

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

    Required for password_reset purpose

Returns:



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/wowsql/auth.rb', line 270

def verify_otp(email:, otp:, purpose: 'login', new_password: nil)
  unless %w[login signup password_reset].include?(purpose)
    raise WOWSQLError.new("Purpose must be 'login', 'signup', or 'password_reset'")
  end

  if purpose == 'password_reset' && new_password.nil?
    raise WOWSQLError.new('new_password is required for password_reset purpose')
  end

  payload = { email: email, otp: otp, purpose: purpose }
  payload[:new_password] = new_password if new_password

  data = request('POST', '/otp/verify', nil, payload)

  if purpose == 'password_reset'
    return {
      'success' => data['success'] != false,
      'message' => data['message'] || 'Password reset successfully! You can now login with your new password'
    }
  end

  session = persist_session(data)
  user = data['user'] ? AuthUser.new(**normalize_user(data['user'])) : nil
  AuthResponse.new(session: session, user: user)
end