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.
- checksums.yaml +4 -4
- data/README.md +359 -417
- data/lib/wowmysql.rb +3 -2
- data/lib/wowsql/auth.rb +313 -164
- data/lib/wowsql/client.rb +54 -25
- data/lib/wowsql/exceptions.rb +15 -14
- data/lib/wowsql/query_builder.rb +229 -120
- data/lib/wowsql/schema.rb +250 -0
- data/lib/wowsql/storage.rb +327 -176
- data/lib/wowsql/table.rb +111 -10
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +2 -2
- metadata +20 -5
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
|
-
#
|
|
13
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
@
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
73
|
-
|
|
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]
|
|
168
|
+
# @return [Hash]
|
|
82
169
|
def get_oauth_authorization_url(provider:, redirect_uri: nil)
|
|
83
|
-
raise
|
|
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
|
|
93
|
-
provider
|
|
94
|
-
backend_callback_url
|
|
95
|
-
frontend_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
|
|
184
|
+
rescue WOWSQLError => e
|
|
98
185
|
if e.status_code == 502
|
|
99
|
-
raise
|
|
100
|
-
"Bad Gateway (502): The backend server may be down or unreachable.
|
|
101
|
-
|
|
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
|
|
106
|
-
"Bad Request (400): #{e.message}.
|
|
107
|
-
|
|
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 [
|
|
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]
|
|
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
|
|
144
|
-
message
|
|
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]
|
|
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
|
|
163
|
-
message
|
|
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]
|
|
171
|
-
# @return [Hash]
|
|
248
|
+
# @param purpose [String] 'login', 'signup', or 'password_reset'
|
|
249
|
+
# @return [Hash]
|
|
172
250
|
def send_otp(email:, purpose: 'login')
|
|
173
|
-
unless [
|
|
174
|
-
raise
|
|
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
|
|
186
|
-
message
|
|
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]
|
|
195
|
-
# @param new_password [String, nil] Required for password_reset purpose
|
|
196
|
-
# @return [Hash]
|
|
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 [
|
|
199
|
-
raise
|
|
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
|
|
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
|
|
218
|
-
message
|
|
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]
|
|
234
|
-
# @return [Hash]
|
|
299
|
+
# @param purpose [String] 'login', 'signup', or 'email_verification'
|
|
300
|
+
# @return [Hash]
|
|
235
301
|
def send_magic_link(email:, purpose: 'login')
|
|
236
|
-
unless [
|
|
237
|
-
raise
|
|
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
|
|
249
|
-
message
|
|
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]
|
|
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
|
|
263
|
-
message
|
|
264
|
-
user
|
|
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]
|
|
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
|
|
278
|
-
message
|
|
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]
|
|
408
|
+
#
|
|
409
|
+
# @return [Hash]
|
|
285
410
|
def get_session
|
|
286
411
|
{
|
|
287
|
-
access_token
|
|
288
|
-
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
|
-
|
|
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
|
|
448
|
+
normalized = normalized[0...-4] if normalized.end_with?('/api')
|
|
317
449
|
return "#{normalized}/api/auth"
|
|
318
450
|
end
|
|
319
451
|
|
|
320
|
-
|
|
321
|
-
if normalized.include?(
|
|
322
|
-
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
|
-
|
|
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
|
|
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
|
|
358
|
-
@refresh_token = session
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|