better_auth 0.10.0 → 0.11.0
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 +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +4 -4
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +79 -28
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +142 -32
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +41 -14
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
|
@@ -98,8 +98,8 @@ module BetterAuth
|
|
|
98
98
|
state: state,
|
|
99
99
|
codeVerifier: code_verifier,
|
|
100
100
|
code_verifier: code_verifier,
|
|
101
|
-
redirectURI: "#{ctx.context.
|
|
102
|
-
redirect_uri: "#{ctx.context.
|
|
101
|
+
redirectURI: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
102
|
+
redirect_uri: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
103
103
|
scopes: body["scopes"],
|
|
104
104
|
loginHint: body["loginHint"] || body["login_hint"]
|
|
105
105
|
})
|
|
@@ -135,31 +135,43 @@ module BetterAuth
|
|
|
135
135
|
if ctx.method == "POST"
|
|
136
136
|
merged = normalize_hash(ctx.query).merge(normalize_hash(ctx.body))
|
|
137
137
|
query = URI.encode_www_form(merged.reject { |_key, value| value.nil? || value.to_s.empty? })
|
|
138
|
-
target = "#{ctx.context.
|
|
138
|
+
target = "#{ctx.context.canonical_base_url}/callback/#{provider_id}"
|
|
139
139
|
target = "#{target}?#{query}" unless query.empty?
|
|
140
140
|
raise ctx.redirect(target)
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
source = ctx.query
|
|
144
144
|
data = normalize_hash(source)
|
|
145
|
-
provider = social_provider(ctx.context, provider_id)
|
|
146
145
|
state = data["state"].to_s
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
default_error_url = ctx.context.options.on_api_error[:error_url] || "#{ctx.context.base_url}/error"
|
|
147
|
+
state_data = begin
|
|
148
|
+
OAuthState.parse(ctx, state)
|
|
149
|
+
rescue OAuthState::Error => error
|
|
150
|
+
raise ctx.redirect(oauth_error_url(error.error_url || default_error_url, error.code))
|
|
151
|
+
end
|
|
152
|
+
error_url = state_data["errorURL"] || state_data["errorCallbackURL"] || default_error_url
|
|
149
153
|
|
|
150
154
|
raise ctx.redirect(oauth_error_url(error_url, data["error"], data["errorDescription"] || data["error_description"])) if data["error"]
|
|
151
|
-
raise ctx.redirect(oauth_error_url(error_url, "oauth_provider_not_found")) unless provider
|
|
152
|
-
raise ctx.redirect(oauth_error_url(error_url, "state_not_found")) unless state_data
|
|
153
|
-
raise ctx.redirect(oauth_error_url(error_url, "state_mismatch")) unless valid_oauth_state_cookie?(ctx, state)
|
|
154
155
|
raise ctx.redirect(oauth_error_url(error_url, "no_code")) if data["code"].to_s.empty?
|
|
156
|
+
provider = social_provider(ctx.context, provider_id)
|
|
157
|
+
raise ctx.redirect(oauth_error_url(error_url, "oauth_provider_not_found")) unless provider
|
|
155
158
|
|
|
156
|
-
tokens =
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
tokens = begin
|
|
160
|
+
call_provider(provider, :validate_authorization_code, {
|
|
161
|
+
code: data["code"],
|
|
162
|
+
codeVerifier: state_data["codeVerifier"],
|
|
163
|
+
code_verifier: state_data["codeVerifier"],
|
|
164
|
+
redirectURI: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
165
|
+
redirect_uri: "#{ctx.context.canonical_base_url}/callback/#{provider_id}"
|
|
166
|
+
})
|
|
167
|
+
rescue APIError => error
|
|
168
|
+
raise if error.status == "FOUND"
|
|
169
|
+
|
|
170
|
+
nil
|
|
171
|
+
rescue => error
|
|
172
|
+
social_log(ctx.context, :error, "OAuth code validation failed: #{error.message}")
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
163
175
|
raise ctx.redirect(oauth_error_url(error_url, "invalid_code")) unless tokens
|
|
164
176
|
|
|
165
177
|
token_data = token_hash(tokens)
|
|
@@ -172,6 +184,7 @@ module BetterAuth
|
|
|
172
184
|
user = user_info[:user] || user_info["user"] if user_info
|
|
173
185
|
raise ctx.redirect(oauth_error_url(error_url, "unable_to_get_user_info")) unless user
|
|
174
186
|
raise ctx.redirect(oauth_error_url(error_url, "email_not_found")) if fetch_value(user, "email").to_s.empty?
|
|
187
|
+
raise ctx.redirect(oauth_error_url(error_url, "unable_to_get_user_info")) if blank_remote_id?(fetch_value(user, "id"))
|
|
175
188
|
|
|
176
189
|
link = state_data["link"] || state_data[:link]
|
|
177
190
|
if link
|
|
@@ -192,7 +205,7 @@ module BetterAuth
|
|
|
192
205
|
)
|
|
193
206
|
raise ctx.redirect(oauth_error_url(error_url, session_data[:error].tr(" ", "_"))) if session_data[:error]
|
|
194
207
|
Cookies.set_session_cookie(ctx, session_data)
|
|
195
|
-
callback_url = session_data[:new_user] ? (state_data["newUserCallbackURL"] || state_data["callbackURL"] || "/") : (state_data["callbackURL"] || "/")
|
|
208
|
+
callback_url = session_data[:new_user] ? (state_data["newUserURL"] || state_data["newUserCallbackURL"] || state_data["callbackURL"] || "/") : (state_data["callbackURL"] || "/")
|
|
196
209
|
raise ctx.redirect(callback_url)
|
|
197
210
|
end
|
|
198
211
|
end
|
|
@@ -258,8 +271,15 @@ module BetterAuth
|
|
|
258
271
|
account["providerId"] == provider_id && account["accountId"] == account_id
|
|
259
272
|
end
|
|
260
273
|
unless existing
|
|
261
|
-
|
|
274
|
+
begin
|
|
275
|
+
account = ctx.context.internal_adapter.create_account(token_hash_for_storage(ctx, data[:account]).merge("userId" => session[:user]["id"]))
|
|
276
|
+
raise BetterAuth::Error, "Unable to link social account" unless account
|
|
277
|
+
rescue
|
|
278
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
279
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: "Unable to link social account")
|
|
280
|
+
end
|
|
262
281
|
end
|
|
282
|
+
update_social_user_info_on_link(ctx, session[:user]["id"], data[:user])
|
|
263
283
|
update_verified_email_on_link(ctx, session[:user]["id"], session[:user]["email"], data[:user])
|
|
264
284
|
next ctx.json({url: "", status: true, redirect: false})
|
|
265
285
|
end
|
|
@@ -281,8 +301,8 @@ module BetterAuth
|
|
|
281
301
|
state: state,
|
|
282
302
|
codeVerifier: code_verifier,
|
|
283
303
|
code_verifier: code_verifier,
|
|
284
|
-
redirectURI: "#{ctx.context.
|
|
285
|
-
redirect_uri: "#{ctx.context.
|
|
304
|
+
redirectURI: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
305
|
+
redirect_uri: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
286
306
|
scopes: body["scopes"],
|
|
287
307
|
loginHint: body["loginHint"] || body["login_hint"]
|
|
288
308
|
})
|
|
@@ -313,6 +333,7 @@ module BetterAuth
|
|
|
313
333
|
user = user_info[:user] || user_info["user"] if user_info
|
|
314
334
|
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_GET_USER_INFO"]) unless user
|
|
315
335
|
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["USER_EMAIL_NOT_FOUND"]) if fetch_value(user, "email").to_s.empty?
|
|
336
|
+
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_GET_USER_INFO"]) if blank_remote_id?(fetch_value(user, "id"))
|
|
316
337
|
|
|
317
338
|
{
|
|
318
339
|
user: user,
|
|
@@ -327,46 +348,84 @@ module BetterAuth
|
|
|
327
348
|
}
|
|
328
349
|
end
|
|
329
350
|
|
|
330
|
-
def self.persist_social_user(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false)
|
|
351
|
+
def self.persist_social_user(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false, override_user_info: false)
|
|
331
352
|
email = fetch_value(user_info, "email").to_s.downcase
|
|
332
353
|
account_id = (account_info["accountId"] || account_info[:accountId] || account_info[:account_id] || fetch_value(user_info, "id")).to_s
|
|
333
|
-
|
|
354
|
+
return {error: "unable to get user info"} if blank_remote_id?(account_id)
|
|
355
|
+
|
|
356
|
+
existing = begin
|
|
357
|
+
ctx.context.internal_adapter.find_oauth_user(email, account_id, provider_id)
|
|
358
|
+
rescue
|
|
359
|
+
social_log(ctx.context, :error, "Unable to query social user")
|
|
360
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: "internal server error")
|
|
361
|
+
end
|
|
334
362
|
|
|
335
363
|
if existing && existing[:linked_account]
|
|
336
364
|
user = existing[:user]
|
|
337
365
|
if ctx.context.options.account[:update_account_on_sign_in] != false
|
|
338
366
|
update_data = account_storage_fields(account_info)
|
|
339
|
-
|
|
367
|
+
unless update_data.empty?
|
|
368
|
+
begin
|
|
369
|
+
ctx.context.internal_adapter.update_account(existing[:linked_account]["id"], update_data)
|
|
370
|
+
rescue
|
|
371
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
372
|
+
raise
|
|
373
|
+
end
|
|
374
|
+
end
|
|
340
375
|
end
|
|
341
376
|
verified_user = update_verified_email_on_link(ctx, user["id"], user["email"], user_info)
|
|
342
377
|
user = verified_user if verified_user
|
|
343
378
|
new_user = false
|
|
344
379
|
elsif existing
|
|
345
|
-
|
|
346
|
-
|
|
380
|
+
linkable = linkable_provider?(ctx, provider_id, user_info, implicit: true, local_user: existing[:user])
|
|
381
|
+
unless linkable
|
|
382
|
+
return {error: existing[:user]["banned"] ? "banned" : "account not linked"}
|
|
347
383
|
end
|
|
348
384
|
user = existing[:user]
|
|
349
|
-
ctx.context.
|
|
350
|
-
|
|
351
|
-
|
|
385
|
+
user = ctx.context.adapter.transaction do
|
|
386
|
+
account = begin
|
|
387
|
+
ctx.context.internal_adapter.create_account(account_info.merge("providerId" => provider_id, "accountId" => account_id, "userId" => user["id"]))
|
|
388
|
+
rescue
|
|
389
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
390
|
+
raise
|
|
391
|
+
end
|
|
392
|
+
unless account
|
|
393
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
394
|
+
raise BetterAuth::Error, "Failed to link social account"
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
promoted = promote_verified_email_on_link!(ctx, user, user_info)
|
|
398
|
+
linked_user = promoted || user
|
|
399
|
+
update_social_user_info_on_link(ctx, linked_user["id"], user_info) || linked_user
|
|
400
|
+
end
|
|
352
401
|
new_user = false
|
|
353
402
|
else
|
|
354
403
|
return {error: "signup disabled"} if disable_sign_up
|
|
355
404
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
405
|
+
begin
|
|
406
|
+
provider_fields = Schema.parse_provider_profile_user_input(ctx.context.options, user_info, action: :create)
|
|
407
|
+
created = ctx.context.internal_adapter.create_oauth_user(
|
|
408
|
+
provider_fields.merge(
|
|
409
|
+
email: email,
|
|
410
|
+
name: fetch_value(user_info, "name").to_s,
|
|
411
|
+
image: fetch_value(user_info, "image"),
|
|
412
|
+
emailVerified: !!fetch_value(user_info, "emailVerified")
|
|
413
|
+
),
|
|
414
|
+
account_info.merge("providerId" => provider_id, "accountId" => account_id),
|
|
415
|
+
context: ctx
|
|
416
|
+
)
|
|
417
|
+
raise BetterAuth::Error, "Failed to create social user" unless created
|
|
418
|
+
rescue
|
|
419
|
+
social_log(ctx.context, :error, "Unable to create social user")
|
|
420
|
+
return {error: "unable to create user"}
|
|
421
|
+
end
|
|
366
422
|
user = created[:user]
|
|
367
423
|
new_user = true
|
|
368
424
|
end
|
|
369
|
-
|
|
425
|
+
if existing && (override_user_info || provider_override_user_info_on_sign_in?(provider_id, ctx.context))
|
|
426
|
+
user = override_social_user_info(ctx, user, user_info)
|
|
427
|
+
end
|
|
428
|
+
return {error: "unable to create user"} unless user
|
|
370
429
|
|
|
371
430
|
session = ctx.context.internal_adapter.create_session(user["id"], false, session_overrides(ctx), true, ctx)
|
|
372
431
|
{session: session, user: user, new_user: new_user}
|
|
@@ -405,17 +464,32 @@ module BetterAuth
|
|
|
405
464
|
!!(fetch_value(provider, "disableSignUp") || fetch_value(fetch_value(provider, "options") || {}, "disableSignUp"))
|
|
406
465
|
end
|
|
407
466
|
|
|
408
|
-
def self.linkable_provider?(ctx, provider_id, user_info, implicit: false)
|
|
467
|
+
def self.linkable_provider?(ctx, provider_id, user_info, implicit: false, local_user: nil)
|
|
409
468
|
linking = ctx.context.options.account[:account_linking] || {}
|
|
410
469
|
return false if linking[:enabled] == false
|
|
411
470
|
return false if implicit && linking[:disable_implicit_linking] == true
|
|
471
|
+
if implicit && linking.fetch(:require_local_email_verified, true) && !local_user&.fetch("emailVerified", false)
|
|
472
|
+
return false
|
|
473
|
+
end
|
|
412
474
|
|
|
413
475
|
trusted = Array(linking[:trusted_providers]).map(&:to_s).include?(provider_id.to_s)
|
|
414
476
|
trusted || !!fetch_value(user_info, "emailVerified")
|
|
415
477
|
end
|
|
416
478
|
|
|
479
|
+
def self.implicit_link_allowed?(ctx, local_user)
|
|
480
|
+
linking = ctx.context.options.account[:account_linking] || {}
|
|
481
|
+
return false if linking[:enabled] == false || linking[:disable_implicit_linking] == true
|
|
482
|
+
|
|
483
|
+
linking[:require_local_email_verified] == false || !!local_user["emailVerified"]
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def self.blank_remote_id?(value)
|
|
487
|
+
value.nil? || value.to_s.strip.empty?
|
|
488
|
+
end
|
|
489
|
+
|
|
417
490
|
def self.link_social_account_from_callback(ctx, provider_id, user_info, tokens, link)
|
|
418
491
|
return {error: "unable_to_link_account"} unless linkable_provider?(ctx, provider_id, user_info)
|
|
492
|
+
return {error: "unable_to_link_account"} if blank_remote_id?(fetch_value(user_info, "id"))
|
|
419
493
|
|
|
420
494
|
email = fetch_value(user_info, "email").to_s.downcase
|
|
421
495
|
link_email = fetch_value(link, "email").to_s.downcase
|
|
@@ -431,20 +505,21 @@ module BetterAuth
|
|
|
431
505
|
"userId" => user_id
|
|
432
506
|
)
|
|
433
507
|
existing = ctx.context.internal_adapter.find_account_by_provider_id(account_id, provider_id)
|
|
434
|
-
|
|
435
|
-
|
|
508
|
+
begin
|
|
509
|
+
account = if existing
|
|
510
|
+
return {error: "account_already_linked_to_different_user"} if existing["userId"].to_s != user_id
|
|
436
511
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
512
|
+
ctx.context.internal_adapter.update_account(existing["id"], account_info)
|
|
513
|
+
else
|
|
514
|
+
ctx.context.internal_adapter.create_account(account_info)
|
|
515
|
+
end
|
|
516
|
+
raise BetterAuth::Error, "Unable to link social account" unless account
|
|
517
|
+
rescue
|
|
518
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
519
|
+
return {error: "unable_to_link_account"}
|
|
440
520
|
end
|
|
441
521
|
|
|
442
|
-
|
|
443
|
-
ctx.context.internal_adapter.update_user(user_id, {
|
|
444
|
-
name: fetch_value(user_info, "name"),
|
|
445
|
-
image: fetch_value(user_info, "image")
|
|
446
|
-
}.compact)
|
|
447
|
-
end
|
|
522
|
+
update_social_user_info_on_link(ctx, user_id, user_info)
|
|
448
523
|
update_verified_email_on_link(ctx, user_id, link_email, user_info)
|
|
449
524
|
|
|
450
525
|
{status: true}
|
|
@@ -462,13 +537,33 @@ module BetterAuth
|
|
|
462
537
|
else
|
|
463
538
|
!!fetch_value(user_info, "emailVerified")
|
|
464
539
|
end
|
|
465
|
-
|
|
540
|
+
provider_fields = Schema.parse_provider_profile_user_input(ctx.context.options, user_info, action: :update)
|
|
541
|
+
update = provider_fields.merge(
|
|
466
542
|
"email" => email,
|
|
467
543
|
"name" => fetch_value(user_info, "name").to_s,
|
|
468
544
|
"image" => fetch_value(user_info, "image"),
|
|
469
545
|
"emailVerified" => email_verified
|
|
470
|
-
|
|
471
|
-
ctx.context.internal_adapter.update_user(user["id"], update)
|
|
546
|
+
).reject { |_key, value| value.nil? }
|
|
547
|
+
ctx.context.internal_adapter.update_user(user["id"], update)
|
|
548
|
+
rescue
|
|
549
|
+
social_log(ctx.context, :warn, "Could not override social user info")
|
|
550
|
+
raise
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def self.update_social_user_info_on_link(ctx, user_id, user_info)
|
|
554
|
+
return unless ctx.context.options.account.dig(:account_linking, :update_user_info_on_link)
|
|
555
|
+
|
|
556
|
+
provider_fields = Schema.parse_provider_profile_user_input(ctx.context.options, user_info, action: :update)
|
|
557
|
+
update = provider_fields.merge(
|
|
558
|
+
"name" => fetch_value(user_info, "name"),
|
|
559
|
+
"image" => fetch_value(user_info, "image")
|
|
560
|
+
).reject { |_key, value| value.nil? }
|
|
561
|
+
return if update.empty?
|
|
562
|
+
|
|
563
|
+
ctx.context.internal_adapter.update_user(user_id, update)
|
|
564
|
+
rescue
|
|
565
|
+
social_log(ctx.context, :warn, "Could not update user info on account link")
|
|
566
|
+
nil
|
|
472
567
|
end
|
|
473
568
|
|
|
474
569
|
def self.safe_additional_state(body)
|
|
@@ -507,6 +602,17 @@ module BetterAuth
|
|
|
507
602
|
ctx.context.internal_adapter.update_user(user_id, {"emailVerified" => true})
|
|
508
603
|
end
|
|
509
604
|
|
|
605
|
+
def self.promote_verified_email_on_link!(ctx, local_user, social_user)
|
|
606
|
+
return if local_user["emailVerified"] == true
|
|
607
|
+
return unless fetch_value(social_user, "emailVerified")
|
|
608
|
+
return unless fetch_value(social_user, "email").to_s.strip.downcase == local_user["email"].to_s.strip.downcase
|
|
609
|
+
|
|
610
|
+
updated = ctx.context.internal_adapter.update_user(local_user.fetch("id"), {"emailVerified" => true})
|
|
611
|
+
raise BetterAuth::Error, "Failed to promote verified email" unless updated
|
|
612
|
+
|
|
613
|
+
updated
|
|
614
|
+
end
|
|
615
|
+
|
|
510
616
|
def self.parse_json_hash(value)
|
|
511
617
|
return value if value.is_a?(Hash)
|
|
512
618
|
return {} if value.nil? || value.to_s.empty?
|
|
@@ -516,5 +622,14 @@ module BetterAuth
|
|
|
516
622
|
rescue JSON::ParserError
|
|
517
623
|
{}
|
|
518
624
|
end
|
|
625
|
+
|
|
626
|
+
def self.social_log(context, level, message)
|
|
627
|
+
logger = context.logger
|
|
628
|
+
if logger.respond_to?(:call)
|
|
629
|
+
logger.call(level, message)
|
|
630
|
+
elsif logger.respond_to?(level)
|
|
631
|
+
logger.public_send(level, message)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
519
634
|
end
|
|
520
635
|
end
|
|
@@ -89,7 +89,7 @@ module BetterAuth
|
|
|
89
89
|
ctx.context.internal_adapter.update_account(account["id"], password: hash_password(ctx, new_password))
|
|
90
90
|
token = nil
|
|
91
91
|
if body["revokeOtherSessions"] || body["revoke_other_sessions"]
|
|
92
|
-
ctx.context.internal_adapter.
|
|
92
|
+
ctx.context.internal_adapter.delete_user_sessions(session[:user]["id"])
|
|
93
93
|
new_session = ctx.context.internal_adapter.create_session(session[:user]["id"])
|
|
94
94
|
Cookies.set_session_cookie(ctx, {session: new_session, user: session[:user]})
|
|
95
95
|
token = new_session["token"]
|
|
@@ -176,6 +176,7 @@ module BetterAuth
|
|
|
176
176
|
session = current_session(ctx, sensitive: true)
|
|
177
177
|
body = normalize_hash(ctx.body)
|
|
178
178
|
sender = ctx.context.options.user.dig(:delete_user, :send_delete_account_verification)
|
|
179
|
+
token_link_base_url = ctx.context.token_link_base_url if !body["token"] && sender.respond_to?(:call)
|
|
179
180
|
if body["password"]
|
|
180
181
|
account = credential_account(ctx, session[:user]["id"])
|
|
181
182
|
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["CREDENTIAL_ACCOUNT_NOT_FOUND"]) unless account && account["password"]
|
|
@@ -191,7 +192,7 @@ module BetterAuth
|
|
|
191
192
|
token = SecureRandom.hex(16)
|
|
192
193
|
expires_in = ctx.context.options.user.dig(:delete_user, :delete_token_expires_in) || 3600
|
|
193
194
|
callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] || "/"
|
|
194
|
-
url = "#{
|
|
195
|
+
url = "#{token_link_base_url}/delete-user/callback?token=#{URI.encode_www_form_component(token)}&callbackURL=#{URI.encode_www_form_component(callback_url)}"
|
|
195
196
|
ctx.context.internal_adapter.create_verification_value(
|
|
196
197
|
identifier: "delete-account-#{token}",
|
|
197
198
|
value: session[:user]["id"],
|
|
@@ -298,7 +299,7 @@ module BetterAuth
|
|
|
298
299
|
}
|
|
299
300
|
) do |ctx|
|
|
300
301
|
enabled = ctx.context.options.user.dig(:change_email, :enabled)
|
|
301
|
-
raise APIError.new("BAD_REQUEST",
|
|
302
|
+
raise APIError.new("BAD_REQUEST", code: "CHANGE_EMAIL_DISABLED", message: BASE_ERROR_CODES["CHANGE_EMAIL_DISABLED"]) unless enabled
|
|
302
303
|
session = current_session(ctx, sensitive: true)
|
|
303
304
|
body = normalize_hash(ctx.body)
|
|
304
305
|
new_email = (body["newEmail"] || body["new_email"]).to_s.downcase
|
|
@@ -312,6 +313,7 @@ module BetterAuth
|
|
|
312
313
|
unless can_update_without_verification || can_send_confirmation || can_send_verification
|
|
313
314
|
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VERIFICATION_EMAIL_NOT_ENABLED"])
|
|
314
315
|
end
|
|
316
|
+
ctx.context.token_link_base_url if can_send_confirmation || can_send_verification
|
|
315
317
|
|
|
316
318
|
existing_target = ctx.context.internal_adapter.find_user_by_email(new_email)
|
|
317
319
|
next ctx.json({status: true}) if existing_target
|
|
@@ -326,6 +328,7 @@ module BetterAuth
|
|
|
326
328
|
if can_send_confirmation
|
|
327
329
|
callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"]
|
|
328
330
|
token = create_email_verification_token(ctx, session[:user]["email"], update_to: new_email, extra: {"requestType" => "change-email-confirmation"})
|
|
331
|
+
register_change_email_token!(ctx, token)
|
|
329
332
|
url = email_verification_url(ctx, token, callback_url)
|
|
330
333
|
confirmation_sender.call({user: session[:user], new_email: new_email, url: url, token: token}, ctx.request)
|
|
331
334
|
next ctx.json({status: true})
|
|
@@ -338,20 +341,20 @@ module BetterAuth
|
|
|
338
341
|
|
|
339
342
|
def self.send_change_email_verification(ctx, sender, user, current_email, new_email, callback_url)
|
|
340
343
|
token = create_email_verification_token(ctx, current_email, update_to: new_email, extra: {"requestType" => "change-email-verification"})
|
|
344
|
+
register_change_email_token!(ctx, token)
|
|
341
345
|
sender.call({user: user.merge("email" => new_email), url: email_verification_url(ctx, token, callback_url), token: token}, ctx.request)
|
|
342
346
|
end
|
|
343
347
|
|
|
344
348
|
def self.email_verification_url(ctx, token, callback_url)
|
|
345
349
|
callback = URI.encode_www_form_component(callback_url || "/")
|
|
346
|
-
"#{ctx.context.
|
|
350
|
+
"#{ctx.context.token_link_base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}"
|
|
347
351
|
end
|
|
348
352
|
|
|
349
353
|
def self.delete_user_by_token!(ctx, session, token)
|
|
350
|
-
verification = ctx.context.internal_adapter.
|
|
351
|
-
unless verification && verification["value"] == session[:user]["id"]
|
|
354
|
+
verification = ctx.context.internal_adapter.consume_verification_value("delete-account-#{token}")
|
|
355
|
+
unless verification && verification["value"] == session[:user]["id"]
|
|
352
356
|
raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["INVALID_TOKEN"])
|
|
353
357
|
end
|
|
354
|
-
ctx.context.internal_adapter.delete_verification_value(verification["id"])
|
|
355
358
|
end
|
|
356
359
|
|
|
357
360
|
def self.delete_current_user!(ctx, session)
|
|
@@ -360,7 +363,7 @@ module BetterAuth
|
|
|
360
363
|
deleted = ctx.context.internal_adapter.delete_user(session[:user]["id"])
|
|
361
364
|
raise APIError.new("BAD_REQUEST", message: "User delete aborted") if deleted == false
|
|
362
365
|
|
|
363
|
-
ctx.context.internal_adapter.
|
|
366
|
+
ctx.context.internal_adapter.delete_user_sessions(session[:user]["id"])
|
|
364
367
|
Cookies.delete_session_cookie(ctx)
|
|
365
368
|
call_option(config[:after_delete], session[:user], ctx.request)
|
|
366
369
|
end
|
|
@@ -7,18 +7,22 @@ module BetterAuth
|
|
|
7
7
|
|
|
8
8
|
def create_statements(options, dialect:)
|
|
9
9
|
dialect = dialect.to_sym
|
|
10
|
-
tables = Schema.
|
|
11
|
-
|
|
10
|
+
tables = Schema.migration_tables(options)
|
|
11
|
+
delete_actions = mssql_delete_actions(tables, dialect)
|
|
12
|
+
statements = tables.map do |logical_name, table|
|
|
13
|
+
create_table_statement(logical_name, table, dialect, tables, delete_actions: delete_actions)
|
|
14
|
+
end
|
|
12
15
|
statements.concat(tables.flat_map { |_logical_name, table| index_statements(table, dialect) })
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def pending_statements(plan)
|
|
19
|
+
delete_actions = mssql_delete_actions(plan.tables, plan.dialect)
|
|
16
20
|
statements = plan.to_create.map do |change|
|
|
17
|
-
create_table_statement(change.logical_name, change.table, plan.dialect, plan.tables)
|
|
21
|
+
create_table_statement(change.logical_name, change.table, plan.dialect, plan.tables, delete_actions: delete_actions)
|
|
18
22
|
end
|
|
19
23
|
statements.concat(plan.to_add.flat_map do |change|
|
|
20
24
|
change.fields.map do |logical_field, attributes|
|
|
21
|
-
if logical_field
|
|
25
|
+
if logical_name(logical_field, attributes) == "id" && plan.dialect == :postgres
|
|
22
26
|
add_postgres_id_column_statements(change.table_name)
|
|
23
27
|
else
|
|
24
28
|
add_column_statement(change.table_name, logical_field, attributes, plan.dialect)
|
|
@@ -37,13 +41,13 @@ module BetterAuth
|
|
|
37
41
|
end)
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
def create_table_statement(logical_name, table, dialect, tables = nil)
|
|
44
|
+
def create_table_statement(logical_name, table, dialect, tables = nil, delete_actions: nil)
|
|
41
45
|
table_name = table.fetch(:model_name)
|
|
42
46
|
columns = table.fetch(:fields).map do |logical_field, attributes|
|
|
43
47
|
column_definition(table_name, logical_field, attributes, dialect)
|
|
44
48
|
end
|
|
45
49
|
constraints = table.fetch(:fields).flat_map do |logical_field, attributes|
|
|
46
|
-
field_constraints(table_name, logical_field, attributes, dialect, tables)
|
|
50
|
+
field_constraints(table_name, logical_field, attributes, dialect, tables, delete_actions: delete_actions)
|
|
47
51
|
end
|
|
48
52
|
body = (columns + constraints).join(",\n ")
|
|
49
53
|
|
|
@@ -61,8 +65,8 @@ module BetterAuth
|
|
|
61
65
|
|
|
62
66
|
def column_definition(table_name, logical_field, attributes, dialect)
|
|
63
67
|
column = quote(attributes[:field_name] || physical_name(logical_field), dialect)
|
|
64
|
-
parts = [column, sql_type(logical_field, attributes, dialect)]
|
|
65
|
-
parts << "PRIMARY KEY" if logical_field == "id"
|
|
68
|
+
parts = [column, sql_type(logical_name(logical_field, attributes), attributes, dialect)]
|
|
69
|
+
parts << "PRIMARY KEY" if logical_name(logical_field, attributes) == "id"
|
|
66
70
|
if attributes[:required]
|
|
67
71
|
parts << "NOT NULL"
|
|
68
72
|
elsif dialect == :mssql
|
|
@@ -73,17 +77,17 @@ module BetterAuth
|
|
|
73
77
|
parts.join(" ")
|
|
74
78
|
end
|
|
75
79
|
|
|
76
|
-
def field_constraints(table_name, logical_field, attributes, dialect, tables = nil)
|
|
80
|
+
def field_constraints(table_name, logical_field, attributes, dialect, tables = nil, delete_actions: nil)
|
|
77
81
|
constraints = []
|
|
78
82
|
column = attributes[:field_name] || physical_name(logical_field)
|
|
79
83
|
|
|
80
|
-
if attributes[:unique] && logical_field != "id" && !(dialect == :mssql && !attributes[:required])
|
|
84
|
+
if attributes[:unique] && logical_name(logical_field, attributes) != "id" && !(dialect == :mssql && !attributes[:required])
|
|
81
85
|
constraints << unique_constraint(table_name, column, dialect)
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
reference = attributes[:references]
|
|
85
89
|
if reference
|
|
86
|
-
constraints << foreign_key_constraint(table_name, column, reference, dialect, tables)
|
|
90
|
+
constraints << foreign_key_constraint(table_name, column, reference, dialect, tables, delete_actions: delete_actions)
|
|
87
91
|
end
|
|
88
92
|
|
|
89
93
|
constraints
|
|
@@ -92,7 +96,7 @@ module BetterAuth
|
|
|
92
96
|
def index_statements(table, dialect)
|
|
93
97
|
table_name = table.fetch(:model_name)
|
|
94
98
|
table.fetch(:fields).filter_map do |logical_field, attributes|
|
|
95
|
-
nullable_unique_mssql = dialect == :mssql && attributes[:unique] && logical_field != "id" && !attributes[:required]
|
|
99
|
+
nullable_unique_mssql = dialect == :mssql && attributes[:unique] && logical_name(logical_field, attributes) != "id" && !attributes[:required]
|
|
96
100
|
next if attributes[:unique] && !nullable_unique_mssql
|
|
97
101
|
next unless attributes[:index] || nullable_unique_mssql
|
|
98
102
|
|
|
@@ -149,7 +153,8 @@ module BetterAuth
|
|
|
149
153
|
end
|
|
150
154
|
|
|
151
155
|
def sql_type(logical_field, attributes, dialect)
|
|
152
|
-
|
|
156
|
+
type = attributes[:type] || "string"
|
|
157
|
+
case type
|
|
153
158
|
when "boolean"
|
|
154
159
|
case dialect
|
|
155
160
|
when :mysql
|
|
@@ -185,16 +190,22 @@ module BetterAuth
|
|
|
185
190
|
else
|
|
186
191
|
"text"
|
|
187
192
|
end
|
|
193
|
+
when "string"
|
|
194
|
+
indexed_string_sql_type(logical_field, attributes, dialect)
|
|
188
195
|
else
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
raise BetterAuth::Error, "Unsupported field type: #{type}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def indexed_string_sql_type(logical_field, attributes, dialect)
|
|
201
|
+
if dialect == :mysql
|
|
202
|
+
indexed = logical_field == "id" || attributes[:unique] || attributes[:index] || attributes[:references] || attributes[:sortable] || attributes.key?(:default_value)
|
|
203
|
+
indexed ? "varchar(191)" : "text"
|
|
204
|
+
elsif dialect == :mssql
|
|
205
|
+
indexed = logical_field == "id" || attributes[:unique] || attributes[:index] || attributes[:references] || attributes[:sortable]
|
|
206
|
+
indexed ? "varchar(255)" : "varchar(8000)"
|
|
207
|
+
else
|
|
208
|
+
"text"
|
|
198
209
|
end
|
|
199
210
|
end
|
|
200
211
|
|
|
@@ -229,11 +240,12 @@ module BetterAuth
|
|
|
229
240
|
end
|
|
230
241
|
end
|
|
231
242
|
|
|
232
|
-
def foreign_key_constraint(table_name, column, reference, dialect, tables = nil)
|
|
243
|
+
def foreign_key_constraint(table_name, column, reference, dialect, tables = nil, delete_actions: nil)
|
|
233
244
|
target_table = foreign_key_target_table(reference, tables)
|
|
234
245
|
target_model = target_table&.fetch(:model_name) || reference.fetch(:model)
|
|
235
246
|
target_field = foreign_key_target_field(reference, target_table)
|
|
236
|
-
|
|
247
|
+
delete_action = delete_actions&.fetch([table_name.to_s, column.to_s], reference[:on_delete]) || reference[:on_delete]
|
|
248
|
+
on_delete = delete_action ? " ON DELETE #{delete_action.to_s.upcase}" : ""
|
|
237
249
|
|
|
238
250
|
case dialect
|
|
239
251
|
when :postgres, :sqlite
|
|
@@ -245,6 +257,56 @@ module BetterAuth
|
|
|
245
257
|
end
|
|
246
258
|
end
|
|
247
259
|
|
|
260
|
+
def mssql_delete_actions(tables, dialect)
|
|
261
|
+
return unless dialect.to_sym == :mssql
|
|
262
|
+
|
|
263
|
+
graph = {}
|
|
264
|
+
tables.each_with_object({}) do |(_logical_name, table), actions|
|
|
265
|
+
child = table.fetch(:model_name).to_s
|
|
266
|
+
table.fetch(:fields).each do |logical_field, attributes|
|
|
267
|
+
reference = attributes[:references]
|
|
268
|
+
next unless reference
|
|
269
|
+
|
|
270
|
+
column = (attributes[:field_name] || physical_name(logical_field)).to_s
|
|
271
|
+
action = reference[:on_delete]&.to_s
|
|
272
|
+
key = [child, column]
|
|
273
|
+
unless mssql_cascading_action?(action)
|
|
274
|
+
actions[key] = action
|
|
275
|
+
next
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
target_table = foreign_key_target_table(reference, tables)
|
|
279
|
+
parent = (target_table&.fetch(:model_name) || reference.fetch(:model)).to_s
|
|
280
|
+
if mssql_cascade_conflict?(graph, parent, child)
|
|
281
|
+
actions[key] = "no action"
|
|
282
|
+
else
|
|
283
|
+
graph[parent] ||= []
|
|
284
|
+
graph[parent] << child unless graph[parent].include?(child)
|
|
285
|
+
actions[key] = action
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def mssql_cascading_action?(action)
|
|
292
|
+
%w[cascade set\ null set\ default].include?(action.to_s.downcase.tr("_", " "))
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def mssql_cascade_conflict?(graph, parent, child)
|
|
296
|
+
nodes = (graph.keys + graph.values.flatten + [parent, child]).uniq
|
|
297
|
+
nodes.any? do |source|
|
|
298
|
+
mssql_reachable?(graph, source, parent) && mssql_reachable?(graph, source, child)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def mssql_reachable?(graph, source, target, visited = {})
|
|
303
|
+
return true if source == target
|
|
304
|
+
return false if visited[source]
|
|
305
|
+
|
|
306
|
+
visited[source] = true
|
|
307
|
+
Array(graph[source]).any? { |child| mssql_reachable?(graph, child, target, visited) }
|
|
308
|
+
end
|
|
309
|
+
|
|
248
310
|
def foreign_key_target_table(reference, tables)
|
|
249
311
|
return unless tables
|
|
250
312
|
|
|
@@ -286,6 +348,10 @@ module BetterAuth
|
|
|
286
348
|
.tr("-", "_")
|
|
287
349
|
.downcase
|
|
288
350
|
end
|
|
351
|
+
|
|
352
|
+
def logical_name(logical_field, attributes)
|
|
353
|
+
(attributes[:logical_name] || logical_field).to_s
|
|
354
|
+
end
|
|
289
355
|
end
|
|
290
356
|
end
|
|
291
357
|
end
|