better_auth 0.2.0 → 0.4.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 +32 -0
- data/README.md +5 -3
- data/lib/better_auth/adapters/internal_adapter.rb +173 -20
- data/lib/better_auth/adapters/memory.rb +61 -12
- data/lib/better_auth/adapters/mongodb.rb +5 -365
- data/lib/better_auth/adapters/sql.rb +44 -3
- data/lib/better_auth/api.rb +7 -2
- data/lib/better_auth/async.rb +70 -0
- data/lib/better_auth/context.rb +2 -1
- data/lib/better_auth/database_hooks.rb +3 -3
- data/lib/better_auth/deprecate.rb +28 -0
- data/lib/better_auth/endpoint.rb +5 -2
- data/lib/better_auth/host.rb +166 -0
- data/lib/better_auth/instrumentation.rb +74 -0
- data/lib/better_auth/logger.rb +31 -0
- data/lib/better_auth/middleware/origin_check.rb +2 -2
- data/lib/better_auth/oauth2.rb +94 -0
- data/lib/better_auth/plugin.rb +14 -1
- data/lib/better_auth/plugins/email_otp.rb +16 -5
- data/lib/better_auth/plugins/generic_oauth.rb +14 -28
- data/lib/better_auth/plugins/oauth_protocol.rb +553 -64
- data/lib/better_auth/plugins/organization/schema.rb +6 -0
- data/lib/better_auth/plugins/organization.rb +56 -20
- data/lib/better_auth/plugins/two_factor.rb +53 -18
- data/lib/better_auth/rate_limiter.rb +37 -2
- data/lib/better_auth/request_state.rb +44 -0
- data/lib/better_auth/router.rb +14 -1
- data/lib/better_auth/routes/account.rb +16 -4
- data/lib/better_auth/routes/email_verification.rb +5 -2
- data/lib/better_auth/routes/password.rb +21 -1
- data/lib/better_auth/routes/session.rb +27 -4
- data/lib/better_auth/routes/sign_in.rb +3 -1
- data/lib/better_auth/routes/sign_up.rb +60 -1
- data/lib/better_auth/routes/social.rb +231 -22
- data/lib/better_auth/routes/user.rb +23 -5
- data/lib/better_auth/schema/sql.rb +11 -0
- data/lib/better_auth/schema.rb +16 -0
- data/lib/better_auth/session.rb +12 -1
- data/lib/better_auth/social_providers/apple.rb +44 -8
- data/lib/better_auth/social_providers/atlassian.rb +32 -0
- data/lib/better_auth/social_providers/base.rb +262 -4
- data/lib/better_auth/social_providers/cognito.rb +32 -0
- data/lib/better_auth/social_providers/discord.rb +27 -5
- data/lib/better_auth/social_providers/dropbox.rb +33 -0
- data/lib/better_auth/social_providers/facebook.rb +35 -0
- data/lib/better_auth/social_providers/figma.rb +31 -0
- data/lib/better_auth/social_providers/github.rb +21 -6
- data/lib/better_auth/social_providers/gitlab.rb +16 -3
- data/lib/better_auth/social_providers/google.rb +38 -13
- data/lib/better_auth/social_providers/huggingface.rb +31 -0
- data/lib/better_auth/social_providers/kakao.rb +32 -0
- data/lib/better_auth/social_providers/kick.rb +32 -0
- data/lib/better_auth/social_providers/line.rb +33 -0
- data/lib/better_auth/social_providers/linear.rb +44 -0
- data/lib/better_auth/social_providers/linkedin.rb +30 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +79 -7
- data/lib/better_auth/social_providers/naver.rb +31 -0
- data/lib/better_auth/social_providers/notion.rb +33 -0
- data/lib/better_auth/social_providers/paybin.rb +31 -0
- data/lib/better_auth/social_providers/paypal.rb +36 -0
- data/lib/better_auth/social_providers/polar.rb +31 -0
- data/lib/better_auth/social_providers/railway.rb +49 -0
- data/lib/better_auth/social_providers/reddit.rb +32 -0
- data/lib/better_auth/social_providers/roblox.rb +31 -0
- data/lib/better_auth/social_providers/salesforce.rb +38 -0
- data/lib/better_auth/social_providers/slack.rb +30 -0
- data/lib/better_auth/social_providers/spotify.rb +31 -0
- data/lib/better_auth/social_providers/tiktok.rb +35 -0
- data/lib/better_auth/social_providers/twitch.rb +39 -0
- data/lib/better_auth/social_providers/twitter.rb +32 -0
- data/lib/better_auth/social_providers/vercel.rb +47 -0
- data/lib/better_auth/social_providers/vk.rb +34 -0
- data/lib/better_auth/social_providers/wechat.rb +104 -0
- data/lib/better_auth/social_providers/zoom.rb +31 -0
- data/lib/better_auth/social_providers.rb +29 -0
- data/lib/better_auth/url_helpers.rb +195 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +8 -1
- metadata +38 -15
|
@@ -10,6 +10,7 @@ module BetterAuth
|
|
|
10
10
|
organization: {
|
|
11
11
|
model_name: "organizations",
|
|
12
12
|
fields: {
|
|
13
|
+
id: {type: "string", required: true},
|
|
13
14
|
name: {type: "string", required: true, sortable: true},
|
|
14
15
|
slug: {type: "string", required: true, unique: true, sortable: true, index: true},
|
|
15
16
|
logo: {type: "string", required: false},
|
|
@@ -21,6 +22,7 @@ module BetterAuth
|
|
|
21
22
|
member: {
|
|
22
23
|
model_name: "members",
|
|
23
24
|
fields: {
|
|
25
|
+
id: {type: "string", required: true},
|
|
24
26
|
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
25
27
|
userId: {type: "string", required: true, references: {model: "user", field: "id"}, index: true},
|
|
26
28
|
role: {type: "string", required: true, default_value: "member", sortable: true},
|
|
@@ -30,6 +32,7 @@ module BetterAuth
|
|
|
30
32
|
invitation: {
|
|
31
33
|
model_name: "invitations",
|
|
32
34
|
fields: {
|
|
35
|
+
id: {type: "string", required: true},
|
|
33
36
|
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
34
37
|
email: {type: "string", required: true, sortable: true, index: true},
|
|
35
38
|
role: {type: "string", required: true, sortable: true},
|
|
@@ -50,6 +53,7 @@ module BetterAuth
|
|
|
50
53
|
schema[:team] = {
|
|
51
54
|
model_name: "teams",
|
|
52
55
|
fields: {
|
|
56
|
+
id: {type: "string", required: true},
|
|
53
57
|
name: {type: "string", required: true},
|
|
54
58
|
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
55
59
|
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
@@ -59,6 +63,7 @@ module BetterAuth
|
|
|
59
63
|
schema[:teamMember] = {
|
|
60
64
|
model_name: "team_members",
|
|
61
65
|
fields: {
|
|
66
|
+
id: {type: "string", required: true},
|
|
62
67
|
teamId: {type: "string", required: true, references: {model: "team", field: "id"}, index: true},
|
|
63
68
|
userId: {type: "string", required: true, references: {model: "user", field: "id"}, index: true},
|
|
64
69
|
createdAt: {type: "date", required: false, default_value: -> { Time.now }}
|
|
@@ -72,6 +77,7 @@ module BetterAuth
|
|
|
72
77
|
schema[:organizationRole] = {
|
|
73
78
|
model_name: "organization_roles",
|
|
74
79
|
fields: {
|
|
80
|
+
id: {type: "string", required: true},
|
|
75
81
|
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
76
82
|
role: {type: "string", required: true, index: true},
|
|
77
83
|
permission: {type: "string", required: true},
|
|
@@ -272,6 +272,12 @@ module BetterAuth
|
|
|
272
272
|
Endpoint.new(path: "/organization/set-active", method: "POST") do |ctx|
|
|
273
273
|
session = Routes.current_session(ctx, sensitive: true)
|
|
274
274
|
body = normalize_hash(ctx.body)
|
|
275
|
+
if body.key?(:organization_id) && body[:organization_id].nil?
|
|
276
|
+
updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeOrganizationId: nil, activeTeamId: nil})
|
|
277
|
+
Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeOrganizationId" => nil, "activeTeamId" => nil), user: session[:user]})
|
|
278
|
+
next ctx.json(nil)
|
|
279
|
+
end
|
|
280
|
+
|
|
275
281
|
organization = organization_by_id(ctx, body[:organization_id]) || organization_by_slug(ctx, body[:organization_slug])
|
|
276
282
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
|
|
277
283
|
require_member!(ctx, session[:user]["id"], organization["id"])
|
|
@@ -285,11 +291,16 @@ module BetterAuth
|
|
|
285
291
|
Endpoint.new(path: "/organization/get-full-organization", method: "GET") do |ctx|
|
|
286
292
|
session = Routes.current_session(ctx)
|
|
287
293
|
query = normalize_hash(ctx.query)
|
|
294
|
+
explicit_lookup = query.key?(:organization_slug) || query.key?(:organization_id)
|
|
288
295
|
organization = organization_by_slug(ctx, query[:organization_slug]) || organization_by_id(ctx, query[:organization_id] || session[:session]["activeOrganizationId"])
|
|
289
|
-
|
|
296
|
+
unless organization
|
|
297
|
+
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) if explicit_lookup
|
|
298
|
+
|
|
299
|
+
next ctx.json(nil)
|
|
300
|
+
end
|
|
290
301
|
|
|
291
302
|
require_member!(ctx, session[:user]["id"], organization["id"])
|
|
292
|
-
members = list_members_for(ctx, organization["id"])
|
|
303
|
+
members = list_members_for(ctx, organization["id"], {limit: query[:members_limit] || config[:membership_limit]})
|
|
293
304
|
invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}])
|
|
294
305
|
result = organization_wire(ctx, organization).merge(
|
|
295
306
|
members: members.fetch(:members),
|
|
@@ -307,7 +318,7 @@ module BetterAuth
|
|
|
307
318
|
Endpoint.new(path: "/organization/invite-member", method: "POST") do |ctx|
|
|
308
319
|
session = Routes.current_session(ctx)
|
|
309
320
|
body = normalize_hash(ctx.body)
|
|
310
|
-
organization = organization_by_id(ctx, body[:organization_id])
|
|
321
|
+
organization = organization_by_id(ctx, body[:organization_id] || session[:session]["activeOrganizationId"]) || organization_by_slug(ctx, body[:organization_slug])
|
|
311
322
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
|
|
312
323
|
require_org_permission!(ctx, config, session, organization["id"], {invitation: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION"))
|
|
313
324
|
email = body[:email].to_s.downcase
|
|
@@ -334,21 +345,26 @@ module BetterAuth
|
|
|
334
345
|
raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_LIMIT_REACHED"))
|
|
335
346
|
end
|
|
336
347
|
team_ids = organization_team_ids(body[:team_id] || body[:team_ids])
|
|
348
|
+
ensure_team_member_capacity!(ctx, config, team_ids)
|
|
349
|
+
invitation_data = {
|
|
350
|
+
organizationId: organization["id"],
|
|
351
|
+
email: email,
|
|
352
|
+
role: role,
|
|
353
|
+
status: "pending",
|
|
354
|
+
expiresAt: Time.now + config[:invitation_expires_in].to_i,
|
|
355
|
+
inviterId: session[:user]["id"],
|
|
356
|
+
teamId: team_ids.any? ? team_ids.join(",") : nil,
|
|
357
|
+
createdAt: Time.now
|
|
358
|
+
}.merge(additional_input(body, :organization_id, :organization_slug, :email, :role, :team_id, :team_ids))
|
|
359
|
+
merge_hook_data!(invitation_data, run_org_hook(config, :before_create_invitation, {invitation: invitation_data, inviter: session[:user], organization: organization_wire(ctx, organization)}, ctx))
|
|
337
360
|
invitation = ctx.context.adapter.create(
|
|
338
361
|
model: "invitation",
|
|
339
|
-
data:
|
|
340
|
-
|
|
341
|
-
email: email,
|
|
342
|
-
role: role,
|
|
343
|
-
status: "pending",
|
|
344
|
-
expiresAt: Time.now + config[:invitation_expires_in].to_i,
|
|
345
|
-
inviterId: session[:user]["id"],
|
|
346
|
-
teamId: team_ids.any? ? team_ids.join(",") : nil,
|
|
347
|
-
createdAt: Time.now
|
|
348
|
-
}
|
|
362
|
+
data: invitation_data,
|
|
363
|
+
force_allow_id: true
|
|
349
364
|
)
|
|
350
365
|
sender = config[:send_invitation_email]
|
|
351
366
|
sender.call({id: invitation["id"], role: role, email: email, organization: organization_wire(ctx, organization), invitation: invitation_wire(ctx, invitation), inviter: require_member!(ctx, session[:user]["id"], organization["id"])}, ctx.request) if sender.respond_to?(:call)
|
|
367
|
+
run_org_hook(config, :after_create_invitation, {invitation: invitation_wire(ctx, invitation), inviter: session[:user], organization: organization_wire(ctx, organization)}, ctx)
|
|
352
368
|
ctx.json(invitation_wire(ctx, invitation))
|
|
353
369
|
end
|
|
354
370
|
end
|
|
@@ -365,11 +381,14 @@ module BetterAuth
|
|
|
365
381
|
if config[:require_email_verification_on_invitation] && !session[:user]["emailVerified"]
|
|
366
382
|
raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION"))
|
|
367
383
|
end
|
|
384
|
+
ensure_team_member_capacity!(ctx, config, organization_team_ids(invitation["teamId"]))
|
|
368
385
|
member = ctx.context.adapter.create(model: "member", data: {organizationId: invitation["organizationId"], userId: session[:user]["id"], role: invitation["role"], createdAt: Time.now})
|
|
369
386
|
organization_team_ids(invitation["teamId"]).each do |team_id|
|
|
370
387
|
ctx.context.adapter.create(model: "teamMember", data: {teamId: team_id, userId: session[:user]["id"], createdAt: Time.now})
|
|
371
388
|
end
|
|
372
389
|
updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "accepted"})
|
|
390
|
+
organization = organization_by_id(ctx, invitation["organizationId"])
|
|
391
|
+
run_org_hook(config, :after_accept_invitation, {invitation: invitation_wire(ctx, updated), member: member_wire(ctx, member), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
|
|
373
392
|
ctx.json({invitation: invitation_wire(ctx, updated), member: member_wire(ctx, member)})
|
|
374
393
|
end
|
|
375
394
|
end
|
|
@@ -452,8 +471,11 @@ module BetterAuth
|
|
|
452
471
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless member
|
|
453
472
|
require_org_permission!(ctx, config, session, member["organizationId"], {member: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER"))
|
|
454
473
|
ensure_not_last_owner!(ctx, member)
|
|
474
|
+
organization = organization_by_id(ctx, member["organizationId"])
|
|
475
|
+
user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
|
|
455
476
|
ctx.context.adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
|
|
456
477
|
ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "userId", value: member["userId"]}]) if org_truthy?(config.dig(:teams, :enabled))
|
|
478
|
+
run_org_hook(config, :after_remove_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
|
|
457
479
|
ctx.json({status: true})
|
|
458
480
|
end
|
|
459
481
|
end
|
|
@@ -483,10 +505,12 @@ module BetterAuth
|
|
|
483
505
|
def organization_get_active_member_role_endpoint(_config)
|
|
484
506
|
Endpoint.new(path: "/organization/get-active-member-role", method: "GET") do |ctx|
|
|
485
507
|
session = Routes.current_session(ctx)
|
|
486
|
-
|
|
508
|
+
query = normalize_hash(ctx.query)
|
|
509
|
+
organization_id = query[:organization_id] || session[:session]["activeOrganizationId"]
|
|
487
510
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
|
|
488
|
-
|
|
489
|
-
ctx
|
|
511
|
+
require_member!(ctx, session[:user]["id"], organization_id)
|
|
512
|
+
member = require_member!(ctx, query[:user_id] || session[:user]["id"], organization_id)
|
|
513
|
+
ctx.json({role: member["role"], member: member_wire(ctx, member)})
|
|
490
514
|
end
|
|
491
515
|
end
|
|
492
516
|
|
|
@@ -506,7 +530,7 @@ module BetterAuth
|
|
|
506
530
|
Endpoint.new(path: "/organization/list-members", method: "GET") do |ctx|
|
|
507
531
|
session = Routes.current_session(ctx)
|
|
508
532
|
query = normalize_hash(ctx.query)
|
|
509
|
-
organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id")
|
|
533
|
+
organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id") || session[:session]["activeOrganizationId"]
|
|
510
534
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
|
|
511
535
|
require_member!(ctx, session[:user]["id"], organization_id)
|
|
512
536
|
ctx.json(list_members_for(ctx, organization_id, query))
|
|
@@ -538,7 +562,7 @@ module BetterAuth
|
|
|
538
562
|
end
|
|
539
563
|
team_data = {organizationId: organization_id, name: body[:name].to_s, createdAt: Time.now}.merge(additional_input(body, :organization_id, :name))
|
|
540
564
|
merge_hook_data!(team_data, run_org_hook(config, :before_create_team, {team: team_data, user: session[:user], organization: organization_wire(ctx, organization)}, ctx))
|
|
541
|
-
team = ctx.context.adapter.create(model: "team", data: team_data)
|
|
565
|
+
team = ctx.context.adapter.create(model: "team", data: team_data, force_allow_id: true)
|
|
542
566
|
ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
|
|
543
567
|
run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
|
|
544
568
|
ctx.json(team_wire(ctx, team))
|
|
@@ -859,7 +883,7 @@ module BetterAuth
|
|
|
859
883
|
where: where,
|
|
860
884
|
limit: query[:limit],
|
|
861
885
|
offset: query[:offset],
|
|
862
|
-
sort_by: query[:sort_by] ? {field: query[:sort_by], direction: query[:sort_order] || "asc"} : nil
|
|
886
|
+
sort_by: query[:sort_by] ? {field: query[:sort_by], direction: query[:sort_direction] || query[:sort_order] || "asc"} : nil
|
|
863
887
|
)
|
|
864
888
|
{
|
|
865
889
|
members: members.map { |entry| member_wire(ctx, entry) },
|
|
@@ -867,6 +891,18 @@ module BetterAuth
|
|
|
867
891
|
}
|
|
868
892
|
end
|
|
869
893
|
|
|
894
|
+
def ensure_team_member_capacity!(ctx, config, team_ids)
|
|
895
|
+
max_members = config.dig(:teams, :maximum_members_per_team)
|
|
896
|
+
return unless max_members && team_ids.any?
|
|
897
|
+
|
|
898
|
+
team_ids.each do |team_id|
|
|
899
|
+
count = ctx.context.adapter.count(model: "teamMember", where: [{field: "teamId", value: team_id}])
|
|
900
|
+
if count >= max_members.to_i
|
|
901
|
+
raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_MEMBER_LIMIT_REACHED"))
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
|
|
870
906
|
def member_wire(ctx, member)
|
|
871
907
|
data = Schema.parse_output(ctx.context.options, "member", member)
|
|
872
908
|
user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
|
|
@@ -910,7 +946,7 @@ module BetterAuth
|
|
|
910
946
|
team = if custom.respond_to?(:call)
|
|
911
947
|
custom.call(organization_wire(ctx, organization), ctx)
|
|
912
948
|
else
|
|
913
|
-
ctx.context.adapter.create(model: "team", data: team_data)
|
|
949
|
+
ctx.context.adapter.create(model: "team", data: team_data, force_allow_id: true)
|
|
914
950
|
end
|
|
915
951
|
ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
|
|
916
952
|
run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
|
|
@@ -24,6 +24,7 @@ module BetterAuth
|
|
|
24
24
|
TRUST_DEVICE_COOKIE_NAME = "trust_device"
|
|
25
25
|
TRUST_DEVICE_COOKIE_MAX_AGE = 30 * 24 * 60 * 60
|
|
26
26
|
TWO_FACTOR_COOKIE_MAX_AGE = 10 * 60
|
|
27
|
+
TWO_FACTOR_MODEL = "twoFactor"
|
|
27
28
|
|
|
28
29
|
module_function
|
|
29
30
|
|
|
@@ -39,6 +40,8 @@ module BetterAuth
|
|
|
39
40
|
config[:backup_code_options] = {store_backup_codes: "encrypted"}.merge(normalize_hash(config[:backup_code_options]))
|
|
40
41
|
config[:otp_options] = normalize_hash(config[:otp_options])
|
|
41
42
|
config[:totp_options] = normalize_hash(config[:totp_options])
|
|
43
|
+
config[:backup_code_options][:allow_passwordless] = config[:allow_passwordless] unless config[:backup_code_options].key?(:allow_passwordless)
|
|
44
|
+
config[:totp_options][:allow_passwordless] = config[:allow_passwordless] unless config[:totp_options].key?(:allow_passwordless)
|
|
42
45
|
|
|
43
46
|
Plugin.new(
|
|
44
47
|
id: "two-factor",
|
|
@@ -62,7 +65,7 @@ module BetterAuth
|
|
|
62
65
|
}
|
|
63
66
|
]
|
|
64
67
|
},
|
|
65
|
-
schema: two_factor_schema(config
|
|
68
|
+
schema: two_factor_schema(config),
|
|
66
69
|
rate_limit: [
|
|
67
70
|
{
|
|
68
71
|
path_matcher: ->(path) { path.start_with?("/two-factor/") },
|
|
@@ -79,7 +82,7 @@ module BetterAuth
|
|
|
79
82
|
Endpoint.new(path: "/two-factor/enable", method: "POST") do |ctx|
|
|
80
83
|
session = Routes.current_session(ctx, sensitive: true)
|
|
81
84
|
body = normalize_hash(ctx.body)
|
|
82
|
-
two_factor_check_password!(ctx, session[:user]["id"], body[:password])
|
|
85
|
+
two_factor_check_password!(ctx, session[:user]["id"], body[:password], allow_passwordless: config[:allow_passwordless])
|
|
83
86
|
|
|
84
87
|
secret = two_factor_generate_secret
|
|
85
88
|
backup = two_factor_generate_backup_codes(ctx.context.secret, config[:backup_code_options])
|
|
@@ -90,14 +93,18 @@ module BetterAuth
|
|
|
90
93
|
ctx.context.internal_adapter.delete_session(session[:session]["token"])
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
ctx
|
|
96
|
+
existing = two_factor_record(ctx, config, session[:user]["id"])
|
|
97
|
+
verified = (!!existing && existing["verified"] != false) || !!config[:skip_verification_on_enable]
|
|
98
|
+
ctx.context.adapter.delete_many(model: TWO_FACTOR_MODEL, where: [{field: "userId", value: session[:user]["id"]}])
|
|
94
99
|
ctx.context.adapter.create(
|
|
95
|
-
model:
|
|
100
|
+
model: TWO_FACTOR_MODEL,
|
|
96
101
|
data: {
|
|
97
102
|
secret: Crypto.symmetric_encrypt(key: ctx.context.secret, data: secret),
|
|
98
103
|
backupCodes: backup[:stored],
|
|
99
|
-
userId: session[:user]["id"]
|
|
100
|
-
|
|
104
|
+
userId: session[:user]["id"],
|
|
105
|
+
verified: verified
|
|
106
|
+
},
|
|
107
|
+
force_allow_id: true
|
|
101
108
|
)
|
|
102
109
|
|
|
103
110
|
ctx.json({
|
|
@@ -111,10 +118,10 @@ module BetterAuth
|
|
|
111
118
|
Endpoint.new(path: "/two-factor/disable", method: "POST") do |ctx|
|
|
112
119
|
session = Routes.current_session(ctx, sensitive: true)
|
|
113
120
|
body = normalize_hash(ctx.body)
|
|
114
|
-
two_factor_check_password!(ctx, session[:user]["id"], body[:password])
|
|
121
|
+
two_factor_check_password!(ctx, session[:user]["id"], body[:password], allow_passwordless: config[:allow_passwordless])
|
|
115
122
|
|
|
116
123
|
updated_user = ctx.context.internal_adapter.update_user(session[:user]["id"], twoFactorEnabled: false)
|
|
117
|
-
ctx.context.adapter.delete(model:
|
|
124
|
+
ctx.context.adapter.delete(model: TWO_FACTOR_MODEL, where: [{field: "userId", value: updated_user["id"]}])
|
|
118
125
|
new_session = ctx.context.internal_adapter.create_session(updated_user["id"], false)
|
|
119
126
|
Cookies.set_session_cookie(ctx, {session: new_session, user: updated_user})
|
|
120
127
|
ctx.context.internal_adapter.delete_session(session[:session]["token"])
|
|
@@ -142,7 +149,7 @@ module BetterAuth
|
|
|
142
149
|
Endpoint.new(path: "/two-factor/get-totp-uri", method: "POST") do |ctx|
|
|
143
150
|
two_factor_totp_enabled!(config)
|
|
144
151
|
session = Routes.current_session(ctx, sensitive: true)
|
|
145
|
-
two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password])
|
|
152
|
+
two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password], allow_passwordless: config[:totp_options][:allow_passwordless])
|
|
146
153
|
record = two_factor_record(ctx, config, session[:user]["id"])
|
|
147
154
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TOTP_NOT_ENABLED"]) unless record
|
|
148
155
|
|
|
@@ -158,11 +165,22 @@ module BetterAuth
|
|
|
158
165
|
data = two_factor_verification_context(ctx, config)
|
|
159
166
|
record = two_factor_record(ctx, config, data[:session][:user]["id"])
|
|
160
167
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TOTP_NOT_ENABLED"]) unless record
|
|
168
|
+
if !data[:session][:session] && record["verified"] == false
|
|
169
|
+
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TOTP_NOT_ENABLED"])
|
|
170
|
+
end
|
|
161
171
|
|
|
162
172
|
secret = Crypto.symmetric_decrypt(key: ctx.context.secret, data: record["secret"])
|
|
163
173
|
raise APIError.new("UNAUTHORIZED", message: TWO_FACTOR_ERROR_CODES["INVALID_CODE"]) unless two_factor_totp_valid?(secret, body[:code], options: config[:totp_options])
|
|
164
174
|
|
|
165
|
-
if
|
|
175
|
+
if record["verified"] != true
|
|
176
|
+
if !data[:session][:user]["twoFactorEnabled"] && data[:session][:session]
|
|
177
|
+
updated_user = ctx.context.internal_adapter.update_user(data[:session][:user]["id"], twoFactorEnabled: true)
|
|
178
|
+
new_session = ctx.context.internal_adapter.create_session(updated_user["id"], false)
|
|
179
|
+
ctx.context.internal_adapter.delete_session(data[:session][:session]["token"])
|
|
180
|
+
Cookies.set_session_cookie(ctx, {session: new_session, user: updated_user})
|
|
181
|
+
end
|
|
182
|
+
ctx.context.adapter.update(model: TWO_FACTOR_MODEL, where: [{field: "id", value: record["id"]}], update: {verified: true})
|
|
183
|
+
elsif !data[:session][:user]["twoFactorEnabled"] && data[:session][:session]
|
|
166
184
|
updated_user = ctx.context.internal_adapter.update_user(data[:session][:user]["id"], twoFactorEnabled: true)
|
|
167
185
|
new_session = ctx.context.internal_adapter.create_session(updated_user["id"], false)
|
|
168
186
|
ctx.context.internal_adapter.delete_session(data[:session][:session]["token"])
|
|
@@ -242,7 +260,7 @@ module BetterAuth
|
|
|
242
260
|
remaining = codes.reject { |code| code == body[:code].to_s }
|
|
243
261
|
stored = two_factor_store_backup_codes(ctx.context.secret, remaining, config[:backup_code_options])
|
|
244
262
|
updated = ctx.context.adapter.update(
|
|
245
|
-
model:
|
|
263
|
+
model: TWO_FACTOR_MODEL,
|
|
246
264
|
where: [{field: "id", value: record["id"]}, {field: "backupCodes", value: record["backupCodes"]}],
|
|
247
265
|
update: {backupCodes: stored}
|
|
248
266
|
)
|
|
@@ -257,12 +275,12 @@ module BetterAuth
|
|
|
257
275
|
session = Routes.current_session(ctx, sensitive: true)
|
|
258
276
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TWO_FACTOR_NOT_ENABLED"]) unless session[:user]["twoFactorEnabled"]
|
|
259
277
|
|
|
260
|
-
two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password])
|
|
278
|
+
two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password], allow_passwordless: config[:backup_code_options][:allow_passwordless])
|
|
261
279
|
record = two_factor_record(ctx, config, session[:user]["id"])
|
|
262
280
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TWO_FACTOR_NOT_ENABLED"]) unless record
|
|
263
281
|
|
|
264
282
|
backup = two_factor_generate_backup_codes(ctx.context.secret, config[:backup_code_options])
|
|
265
|
-
ctx.context.adapter.update(model:
|
|
283
|
+
ctx.context.adapter.update(model: TWO_FACTOR_MODEL, where: [{field: "id", value: record["id"]}], update: {backupCodes: backup[:stored]})
|
|
266
284
|
ctx.json({status: true, backupCodes: backup[:codes]})
|
|
267
285
|
end
|
|
268
286
|
end
|
|
@@ -277,7 +295,8 @@ module BetterAuth
|
|
|
277
295
|
end
|
|
278
296
|
end
|
|
279
297
|
|
|
280
|
-
def two_factor_schema(
|
|
298
|
+
def two_factor_schema(config = {})
|
|
299
|
+
custom_schema = config[:schema]
|
|
281
300
|
base = {
|
|
282
301
|
user: {
|
|
283
302
|
fields: {
|
|
@@ -288,10 +307,14 @@ module BetterAuth
|
|
|
288
307
|
fields: {
|
|
289
308
|
secret: {type: "string", required: true, returned: false, index: true},
|
|
290
309
|
backupCodes: {type: "string", required: true, returned: false},
|
|
291
|
-
userId: {type: "string", required: true, returned: false, index: true, references: {model: "user", field: "id"}}
|
|
310
|
+
userId: {type: "string", required: true, returned: false, index: true, references: {model: "user", field: "id"}},
|
|
311
|
+
verified: {type: "boolean", required: false, default_value: true, input: false}
|
|
292
312
|
}
|
|
293
313
|
}
|
|
294
314
|
}
|
|
315
|
+
if config[:two_factor_table] && config[:two_factor_table] != TWO_FACTOR_MODEL
|
|
316
|
+
base[:twoFactor][:model_name] = config[:two_factor_table].to_s
|
|
317
|
+
end
|
|
295
318
|
deep_merge_hashes(base, normalize_hash(custom_schema || {}))
|
|
296
319
|
end
|
|
297
320
|
|
|
@@ -311,7 +334,7 @@ module BetterAuth
|
|
|
311
334
|
expiresAt: Time.now + config[:two_factor_cookie_max_age].to_i
|
|
312
335
|
)
|
|
313
336
|
ctx.set_signed_cookie(cookie.name, identifier, ctx.context.secret, cookie.attributes)
|
|
314
|
-
ctx.json({twoFactorRedirect: true})
|
|
337
|
+
ctx.json({twoFactorRedirect: true, twoFactorMethods: two_factor_methods(ctx, config, data[:user]["id"])})
|
|
315
338
|
end
|
|
316
339
|
|
|
317
340
|
def two_factor_verification_context(ctx, config)
|
|
@@ -377,11 +400,23 @@ module BetterAuth
|
|
|
377
400
|
end
|
|
378
401
|
|
|
379
402
|
def two_factor_record(ctx, config, user_id)
|
|
380
|
-
ctx.context.adapter.find_one(model:
|
|
403
|
+
ctx.context.adapter.find_one(model: TWO_FACTOR_MODEL, where: [{field: "userId", value: user_id}])
|
|
381
404
|
end
|
|
382
405
|
|
|
383
|
-
def
|
|
406
|
+
def two_factor_methods(ctx, config, user_id)
|
|
407
|
+
methods = []
|
|
408
|
+
unless config[:totp_options][:disable]
|
|
409
|
+
record = two_factor_record(ctx, config, user_id)
|
|
410
|
+
methods << "totp" if record && record["verified"] != false
|
|
411
|
+
end
|
|
412
|
+
methods << "otp" if config[:otp_options][:send_otp].respond_to?(:call)
|
|
413
|
+
methods
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def two_factor_check_password!(ctx, user_id, password, allow_passwordless: false)
|
|
384
417
|
account = ctx.context.internal_adapter.find_accounts(user_id).find { |entry| entry["providerId"] == "credential" }
|
|
418
|
+
return if allow_passwordless && !account
|
|
419
|
+
|
|
385
420
|
unless account && account["password"] && Routes.verify_password_value(ctx, password.to_s, account["password"])
|
|
386
421
|
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"])
|
|
387
422
|
end
|
|
@@ -4,6 +4,9 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module BetterAuth
|
|
6
6
|
class RateLimiter
|
|
7
|
+
MISSING_CLIENT_IP_WARNING = "Rate limiting skipped: could not determine client IP address. " \
|
|
8
|
+
"Ensure your runtime forwards a trusted client IP header and configure `advanced.ipAddress.ipAddressHeaders` if needed."
|
|
9
|
+
|
|
7
10
|
class MemoryStore
|
|
8
11
|
def initialize
|
|
9
12
|
@entries = {}
|
|
@@ -36,6 +39,7 @@ module BetterAuth
|
|
|
36
39
|
|
|
37
40
|
def initialize
|
|
38
41
|
@memory_store = MemoryStore.new
|
|
42
|
+
@warned_missing_client_ip = false
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
def call(request, context, path)
|
|
@@ -43,6 +47,7 @@ module BetterAuth
|
|
|
43
47
|
return unless config[:enabled]
|
|
44
48
|
|
|
45
49
|
ip = client_ip(request, context.options)
|
|
50
|
+
warn_missing_client_ip(context) unless ip
|
|
46
51
|
return unless ip
|
|
47
52
|
|
|
48
53
|
rule = rate_limit_rule(request, context, config, path)
|
|
@@ -148,18 +153,26 @@ module BetterAuth
|
|
|
148
153
|
def read_storage((type, storage), key)
|
|
149
154
|
data = storage.get(key)
|
|
150
155
|
data = JSON.parse(data) if type == :secondary && data.is_a?(String)
|
|
151
|
-
symbolize_keys(data)
|
|
156
|
+
normalize_rate_limit_data(symbolize_keys(data))
|
|
152
157
|
rescue JSON::ParserError
|
|
153
158
|
nil
|
|
154
159
|
end
|
|
155
160
|
|
|
156
161
|
def write_storage((type, storage), key, data, ttl:, update:)
|
|
157
|
-
value = (type == :secondary) ? JSON.generate(data) : data
|
|
162
|
+
value = (type == :secondary) ? JSON.generate(secondary_storage_data(data)) : data
|
|
158
163
|
return call_secondary_storage_set(storage, key, value, ttl: ttl, update: update) if type == :secondary
|
|
159
164
|
|
|
160
165
|
call_storage_set(storage, key, value, ttl: ttl, update: update)
|
|
161
166
|
end
|
|
162
167
|
|
|
168
|
+
def secondary_storage_data(data)
|
|
169
|
+
{
|
|
170
|
+
key: data[:key],
|
|
171
|
+
count: data[:count],
|
|
172
|
+
lastRequest: (data[:last_request].to_f * 1000).to_i
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
163
176
|
def call_secondary_storage_set(storage, key, value, ttl:, update:)
|
|
164
177
|
storage.set(key, value, ttl)
|
|
165
178
|
rescue ArgumentError
|
|
@@ -188,6 +201,15 @@ module BetterAuth
|
|
|
188
201
|
end
|
|
189
202
|
end
|
|
190
203
|
|
|
204
|
+
def normalize_rate_limit_data(data)
|
|
205
|
+
return data unless data.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
last_request = data[:last_request]
|
|
208
|
+
return data unless last_request.is_a?(Numeric) && last_request > 10_000_000_000
|
|
209
|
+
|
|
210
|
+
data.merge(last_request: last_request / 1000.0)
|
|
211
|
+
end
|
|
212
|
+
|
|
191
213
|
def rate_limit_key(ip, path)
|
|
192
214
|
"#{ip}|#{path}"
|
|
193
215
|
end
|
|
@@ -196,6 +218,19 @@ module BetterAuth
|
|
|
196
218
|
RequestIP.client_ip(request, options)
|
|
197
219
|
end
|
|
198
220
|
|
|
221
|
+
def warn_missing_client_ip(context)
|
|
222
|
+
return if @warned_missing_client_ip
|
|
223
|
+
return if context.options.advanced.dig(:ip_address, :disable_ip_tracking)
|
|
224
|
+
|
|
225
|
+
@warned_missing_client_ip = true
|
|
226
|
+
logger = context.logger
|
|
227
|
+
if logger.respond_to?(:call)
|
|
228
|
+
logger.call(:warn, MISSING_CLIENT_IP_WARNING)
|
|
229
|
+
elsif logger.respond_to?(:warn)
|
|
230
|
+
logger.warn(MISSING_CLIENT_IP_WARNING)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
199
234
|
def matching_plugin_rule(context, path)
|
|
200
235
|
context.options.plugins
|
|
201
236
|
.flat_map { |plugin| Array(plugin[:rate_limit]) }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module RequestState
|
|
5
|
+
THREAD_KEY = :better_auth_request_state_stack
|
|
6
|
+
|
|
7
|
+
State = Struct.new(:ref, :initializer) do
|
|
8
|
+
def get
|
|
9
|
+
store = RequestState.current_store
|
|
10
|
+
store[ref] = initializer.call unless store.key?(ref)
|
|
11
|
+
store[ref]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def set(value)
|
|
15
|
+
RequestState.current_store[ref] = value
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def run(store = {}, &block)
|
|
22
|
+
stack.push(store)
|
|
23
|
+
block.call
|
|
24
|
+
ensure
|
|
25
|
+
stack.pop
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def present?
|
|
29
|
+
!stack.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def current_store
|
|
33
|
+
stack.last || raise("No request state found. Please make sure you are calling this function within a `run` callback.")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def define(&initializer)
|
|
37
|
+
State.new(Object.new.freeze, initializer || -> {})
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stack
|
|
41
|
+
Thread.current[THREAD_KEY] ||= []
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/better_auth/router.rb
CHANGED
|
@@ -64,6 +64,7 @@ module BetterAuth
|
|
|
64
64
|
|
|
65
65
|
body = parse_body(request)
|
|
66
66
|
endpoint_context = build_endpoint_context(request, route_path, query, body, params)
|
|
67
|
+
return run_on_response_chain(forbidden) if server_only?(endpoint)
|
|
67
68
|
|
|
68
69
|
response = @origin_check.call(endpoint_context)
|
|
69
70
|
return run_on_response_chain(response) if response
|
|
@@ -166,13 +167,17 @@ module BetterAuth
|
|
|
166
167
|
request.body.rewind
|
|
167
168
|
return {} if raw.empty?
|
|
168
169
|
|
|
169
|
-
if request.media_type
|
|
170
|
+
if json_media_type?(request.media_type)
|
|
170
171
|
JSON.parse(raw)
|
|
171
172
|
else
|
|
172
173
|
request.POST
|
|
173
174
|
end
|
|
174
175
|
end
|
|
175
176
|
|
|
177
|
+
def json_media_type?(media_type)
|
|
178
|
+
media_type == "application/json" || media_type.to_s.end_with?("+json")
|
|
179
|
+
end
|
|
180
|
+
|
|
176
181
|
def allowed_media_type?(request, endpoint)
|
|
177
182
|
return true unless request_body_method?(request.request_method)
|
|
178
183
|
return true if request.media_type.nil? || request.media_type.empty?
|
|
@@ -350,6 +355,14 @@ module BetterAuth
|
|
|
350
355
|
[415, {"content-type" => "application/json"}, [JSON.generate({error: "Unsupported Media Type"})]]
|
|
351
356
|
end
|
|
352
357
|
|
|
358
|
+
def forbidden
|
|
359
|
+
[403, {"content-type" => "application/json"}, [JSON.generate({error: "Forbidden"})]]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def server_only?(endpoint)
|
|
363
|
+
endpoint.metadata[:server_only] || endpoint.metadata[:SERVER_ONLY] || endpoint.metadata["SERVER_ONLY"]
|
|
364
|
+
end
|
|
365
|
+
|
|
353
366
|
def error_response(error, headers: {})
|
|
354
367
|
Endpoint::Result.new(
|
|
355
368
|
response: error.to_h,
|
|
@@ -90,10 +90,10 @@ module BetterAuth
|
|
|
90
90
|
Cookies.set_account_cookie(ctx, updated || account.merge(token_hash_for_storage(ctx, tokens)))
|
|
91
91
|
ctx.json({
|
|
92
92
|
accessToken: values["accessToken"],
|
|
93
|
-
refreshToken: values["refreshToken"],
|
|
93
|
+
refreshToken: values["refreshToken"] || refresh_token,
|
|
94
94
|
accessTokenExpiresAt: values["accessTokenExpiresAt"],
|
|
95
|
-
refreshTokenExpiresAt: values["refreshTokenExpiresAt"],
|
|
96
|
-
scope:
|
|
95
|
+
refreshTokenExpiresAt: values["refreshTokenExpiresAt"] || account["refreshTokenExpiresAt"],
|
|
96
|
+
scope: values["scope"] || account["scope"],
|
|
97
97
|
idToken: values["idToken"] || account["idToken"],
|
|
98
98
|
providerId: account["providerId"],
|
|
99
99
|
accountId: account["accountId"]
|
|
@@ -167,7 +167,10 @@ module BetterAuth
|
|
|
167
167
|
def self.update_account_tokens(ctx, account, tokens)
|
|
168
168
|
return nil if account["id"].to_s.empty?
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
data = account_token_update_hash(ctx, tokens)
|
|
171
|
+
return nil if data.empty?
|
|
172
|
+
|
|
173
|
+
ctx.context.internal_adapter.update_account(account["id"], data)
|
|
171
174
|
end
|
|
172
175
|
|
|
173
176
|
def self.token_hash(tokens)
|
|
@@ -183,6 +186,15 @@ module BetterAuth
|
|
|
183
186
|
data
|
|
184
187
|
end
|
|
185
188
|
|
|
189
|
+
def self.account_token_update_hash(ctx, tokens)
|
|
190
|
+
account_storage_fields(token_hash_for_storage(ctx, tokens))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.account_storage_fields(data)
|
|
194
|
+
allowed = %w[accessToken refreshToken idToken accessTokenExpiresAt refreshTokenExpiresAt scope]
|
|
195
|
+
token_hash(data).select { |key, value| allowed.include?(key) && !value.nil? }
|
|
196
|
+
end
|
|
197
|
+
|
|
186
198
|
def self.oauth_token_for_storage(ctx, token)
|
|
187
199
|
return token if token.to_s.empty?
|
|
188
200
|
return token unless ctx.context.options.account[:encrypt_oauth_tokens]
|
|
@@ -35,6 +35,7 @@ module BetterAuth
|
|
|
35
35
|
Endpoint.new(path: "/verify-email", method: "GET") do |ctx|
|
|
36
36
|
token = fetch_value(ctx.query, "token").to_s
|
|
37
37
|
callback_url = fetch_value(ctx.query, "callbackURL")
|
|
38
|
+
validate_callback_url!(ctx.context, callback_url)
|
|
38
39
|
payload = verify_email_token(ctx, token, callback_url)
|
|
39
40
|
email = payload["email"].to_s.downcase
|
|
40
41
|
update_to = payload["updateTo"] || payload["update_to"]
|
|
@@ -43,8 +44,10 @@ module BetterAuth
|
|
|
43
44
|
|
|
44
45
|
user = user_data[:user]
|
|
45
46
|
if update_to
|
|
46
|
-
updated = ctx.context.internal_adapter.update_user_by_email(email, email: update_to, emailVerified:
|
|
47
|
-
|
|
47
|
+
updated = ctx.context.internal_adapter.update_user_by_email(email, email: update_to, emailVerified: false)
|
|
48
|
+
updated_user = updated || user.merge("email" => update_to, "emailVerified" => false)
|
|
49
|
+
send_verification_email_payload(ctx, updated_user, callback_url) if ctx.context.options.email_verification[:send_verification_email].respond_to?(:call)
|
|
50
|
+
set_verified_session_cookie(ctx, updated_user)
|
|
48
51
|
next redirect_or_json(ctx, callback_url, {status: true, user: Schema.parse_output(ctx.context.options, "user", updated)})
|
|
49
52
|
end
|
|
50
53
|
|