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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +138 -91
  5. data/lib/better_auth/adapters/base.rb +64 -1
  6. data/lib/better_auth/adapters/internal_adapter.rb +197 -11
  7. data/lib/better_auth/adapters/join_support.rb +10 -0
  8. data/lib/better_auth/adapters/memory.rb +191 -56
  9. data/lib/better_auth/adapters/mssql.rb +7 -3
  10. data/lib/better_auth/adapters/sql.rb +294 -49
  11. data/lib/better_auth/api.rb +3 -24
  12. data/lib/better_auth/configuration.rb +41 -46
  13. data/lib/better_auth/context.rb +43 -67
  14. data/lib/better_auth/cookies.rb +110 -14
  15. data/lib/better_auth/core.rb +0 -2
  16. data/lib/better_auth/crypto.rb +4 -3
  17. data/lib/better_auth/doctor.rb +14 -3
  18. data/lib/better_auth/error.rb +3 -1
  19. data/lib/better_auth/logger.rb +1 -1
  20. data/lib/better_auth/middleware/origin_check.rb +9 -1
  21. data/lib/better_auth/migration_plan.rb +4 -4
  22. data/lib/better_auth/oauth_state.rb +147 -0
  23. data/lib/better_auth/plugin_loader.rb +220 -0
  24. data/lib/better_auth/plugins/access.rb +10 -13
  25. data/lib/better_auth/plugins/admin.rb +79 -28
  26. data/lib/better_auth/plugins/api_key.rb +4 -7
  27. data/lib/better_auth/plugins/bearer.rb +8 -22
  28. data/lib/better_auth/plugins/captcha.rb +24 -6
  29. data/lib/better_auth/plugins/device_authorization.rb +47 -9
  30. data/lib/better_auth/plugins/dub.rb +2 -2
  31. data/lib/better_auth/plugins/email_otp.rb +30 -47
  32. data/lib/better_auth/plugins/generic_oauth.rb +141 -146
  33. data/lib/better_auth/plugins/i18n.rb +215 -0
  34. data/lib/better_auth/plugins/jwt.rb +37 -19
  35. data/lib/better_auth/plugins/magic_link.rb +18 -21
  36. data/lib/better_auth/plugins/oauth_popup.rb +330 -0
  37. data/lib/better_auth/plugins/oauth_protocol.rb +142 -32
  38. data/lib/better_auth/plugins/oauth_provider.rb +4 -7
  39. data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
  40. data/lib/better_auth/plugins/one_tap.rb +61 -60
  41. data/lib/better_auth/plugins/one_time_token.rb +1 -4
  42. data/lib/better_auth/plugins/open_api.rb +35 -7
  43. data/lib/better_auth/plugins/organization.rb +455 -122
  44. data/lib/better_auth/plugins/passkey.rb +4 -7
  45. data/lib/better_auth/plugins/phone_number.rb +24 -26
  46. data/lib/better_auth/plugins/scim.rb +4 -7
  47. data/lib/better_auth/plugins/siwe.rb +126 -16
  48. data/lib/better_auth/plugins/sso.rb +4 -7
  49. data/lib/better_auth/plugins/stripe.rb +4 -7
  50. data/lib/better_auth/plugins/two_factor.rb +186 -21
  51. data/lib/better_auth/plugins/username.rb +56 -13
  52. data/lib/better_auth/plugins.rb +57 -0
  53. data/lib/better_auth/rate_limiter.rb +297 -135
  54. data/lib/better_auth/request_ip.rb +123 -13
  55. data/lib/better_auth/router.rb +3 -3
  56. data/lib/better_auth/routes/account.rb +40 -8
  57. data/lib/better_auth/routes/email_verification.rb +88 -5
  58. data/lib/better_auth/routes/password.rb +5 -6
  59. data/lib/better_auth/routes/session.rb +7 -2
  60. data/lib/better_auth/routes/sign_in.rb +10 -1
  61. data/lib/better_auth/routes/sign_up.rb +45 -22
  62. data/lib/better_auth/routes/social.rb +169 -54
  63. data/lib/better_auth/routes/user.rb +11 -8
  64. data/lib/better_auth/schema/sql.rb +90 -24
  65. data/lib/better_auth/schema.rb +124 -3
  66. data/lib/better_auth/session.rb +27 -9
  67. data/lib/better_auth/session_store.rb +1 -1
  68. data/lib/better_auth/social_providers/apple.rb +6 -1
  69. data/lib/better_auth/social_providers/atlassian.rb +1 -0
  70. data/lib/better_auth/social_providers/base.rb +41 -14
  71. data/lib/better_auth/social_providers/cognito.rb +75 -5
  72. data/lib/better_auth/social_providers/facebook.rb +95 -1
  73. data/lib/better_auth/social_providers/figma.rb +2 -0
  74. data/lib/better_auth/social_providers/google.rb +12 -1
  75. data/lib/better_auth/social_providers/line.rb +35 -1
  76. data/lib/better_auth/social_providers/linear.rb +1 -0
  77. data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
  78. data/lib/better_auth/social_providers/naver.rb +24 -1
  79. data/lib/better_auth/social_providers/notion.rb +1 -0
  80. data/lib/better_auth/social_providers/paybin.rb +25 -1
  81. data/lib/better_auth/social_providers/paypal.rb +118 -2
  82. data/lib/better_auth/social_providers/reddit.rb +25 -4
  83. data/lib/better_auth/social_providers/salesforce.rb +1 -0
  84. data/lib/better_auth/social_providers/twitch.rb +24 -1
  85. data/lib/better_auth/social_providers/twitter.rb +36 -1
  86. data/lib/better_auth/social_providers/vk.rb +34 -1
  87. data/lib/better_auth/social_providers/wechat.rb +6 -4
  88. data/lib/better_auth/sql_migration.rb +5 -5
  89. data/lib/better_auth/version.rb +1 -1
  90. data/lib/better_auth.rb +11 -41
  91. metadata +5 -13
  92. data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
  93. data/lib/better_auth/plugins/mcp/config.rb +0 -51
  94. data/lib/better_auth/plugins/mcp/consent.rb +0 -31
  95. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
  96. data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
  97. data/lib/better_auth/plugins/mcp/registration.rb +0 -31
  98. data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
  99. data/lib/better_auth/plugins/mcp/schema.rb +0 -91
  100. data/lib/better_auth/plugins/mcp/token.rb +0 -108
  101. data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
  102. data/lib/better_auth/plugins/mcp.rb +0 -256
  103. data/lib/better_auth/plugins/oidc_provider.rb +0 -769
@@ -13,6 +13,7 @@ module BetterAuth
13
13
  "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION" => "User is not a member of the organization",
14
14
  "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION" => "You are not allowed to update this organization",
15
15
  "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION" => "You are not allowed to delete this organization",
16
+ "ORGANIZATION_DELETION_DISABLED" => "Organization deletion is disabled",
16
17
  "NO_ACTIVE_ORGANIZATION" => "No active organization",
17
18
  "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION" => "User is already a member of this organization",
18
19
  "MEMBER_NOT_FOUND" => "Member not found",
@@ -20,6 +21,7 @@ module BetterAuth
20
21
  "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM" => "You are not allowed to create a new team",
21
22
  "TEAM_ALREADY_EXISTS" => "Team already exists",
22
23
  "TEAM_NOT_FOUND" => "Team not found",
24
+ "INVALID_TEAM_ID" => "Team IDs cannot contain commas",
23
25
  "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER" => "You cannot leave the organization as the only owner",
24
26
  "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER" => "You cannot leave the organization without an owner",
25
27
  "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER" => "You are not allowed to delete this member",
@@ -28,6 +30,7 @@ module BetterAuth
28
30
  "INVITATION_NOT_FOUND" => "Invitation not found",
29
31
  "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION" => "You are not the recipient of the invitation",
30
32
  "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION" => "Email verification required before accepting or rejecting invitation",
33
+ "EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION" => "Email verification required to view or list invitations for the session email",
31
34
  "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION" => "You are not allowed to cancel this invitation",
32
35
  "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION" => "Inviter is no longer a member of the organization",
33
36
  "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE" => "You are not allowed to invite a user with this role",
@@ -88,7 +91,7 @@ module BetterAuth
88
91
  cancel_invitation: organization_cancel_invitation_endpoint(config),
89
92
  accept_invitation: organization_accept_invitation_endpoint(config),
90
93
  reject_invitation: organization_reject_invitation_endpoint(config),
91
- get_invitation: organization_get_invitation_endpoint,
94
+ get_invitation: organization_get_invitation_endpoint(config),
92
95
  list_invitations: organization_list_invitations_endpoint(config),
93
96
  list_user_invitations: organization_list_user_invitations_endpoint,
94
97
  add_member: organization_add_member_endpoint(config),
@@ -231,7 +234,7 @@ module BetterAuth
231
234
  data = normalize_hash(body[:data] || body)
232
235
  organization = organization_by_id(ctx, id) || organization_by_slug(ctx, body[:organization_slug])
233
236
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
234
- require_org_permission!(ctx, config, session, organization["id"], {organization: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION"))
237
+ member = require_org_permission!(ctx, config, session, organization["id"], {organization: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION"))
235
238
  if data[:slug] && data[:slug].to_s.empty?
236
239
  raise APIError.new("BAD_REQUEST", message: "slug is required")
237
240
  end
@@ -242,29 +245,37 @@ module BetterAuth
242
245
  raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_SLUG_ALREADY_TAKEN")) if existing && existing["id"] != organization["id"]
243
246
  update = additional_input(data, :organization_id, :organizationId, :organization_slug, :data)
244
247
  update[:metadata] = serialize_metadata(update[:metadata]) if update.key?(:metadata)
248
+ merge_hook_data!(update, run_org_hook(config, :before_update_organization, {organization: update, user: session[:user], member: member_wire(ctx, member)}, ctx))
245
249
  updated = ctx.context.adapter.update(model: "organization", where: [{field: "id", value: organization["id"]}], update: update)
246
- run_org_hook(config, :after_update_organization, {organization: organization_wire(ctx, updated), user: session[:user]}, ctx)
250
+ run_org_hook(config, :after_update_organization, {organization: organization_wire(ctx, updated), user: session[:user], member: member_wire(ctx, member)}, ctx)
247
251
  ctx.json(organization_wire(ctx, updated))
248
252
  end
249
253
  end
250
254
 
251
255
  def organization_delete_endpoint(config)
252
256
  Endpoint.new(path: "/organization/delete", method: "POST", metadata: organization_openapi("deleteOrganization", "Delete an organization", response: OpenAPI.status_response_schema)) do |ctx|
257
+ if org_truthy?(config[:disable_organization_deletion])
258
+ raise APIError.new("NOT_FOUND", code: "ORGANIZATION_DELETION_DISABLED", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_DELETION_DISABLED"))
259
+ end
253
260
  session = Routes.current_session(ctx)
254
261
  body = normalize_hash(ctx.body)
255
262
  organization = organization_by_id(ctx, body[:organization_id]) || organization_by_slug(ctx, body[:organization_slug])
256
263
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
257
264
  require_org_permission!(ctx, config, session, organization["id"], {organization: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION"))
258
265
  run_org_hook(config, :before_delete_organization, {organization: organization_wire(ctx, organization), user: session[:user]}, ctx)
259
- if org_truthy?(config.dig(:teams, :enabled))
260
- team_ids = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: organization["id"]}]).map { |team| team["id"] }
261
- ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team_ids, operator: "in"}]) if team_ids.any?
262
- ctx.context.adapter.delete_many(model: "team", where: [{field: "organizationId", value: organization["id"]}])
266
+ organization_transaction(ctx) do |adapter|
267
+ if org_truthy?(config.dig(:teams, :enabled))
268
+ team_ids = adapter.find_many(model: "team", where: [{field: "organizationId", value: organization["id"]}]).map { |team| team["id"] }
269
+ adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team_ids, operator: "in"}]) if team_ids.any?
270
+ adapter.delete_many(model: "team", where: [{field: "organizationId", value: organization["id"]}])
271
+ end
272
+ adapter.delete_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}])
273
+ adapter.delete_many(model: "member", where: [{field: "organizationId", value: organization["id"]}])
274
+ adapter.delete_many(model: "organizationRole", where: [{field: "organizationId", value: organization["id"]}]) if org_truthy?(config.dig(:dynamic_access_control, :enabled))
275
+ adapter.delete(model: "organization", where: [{field: "id", value: organization["id"]}])
263
276
  end
264
- ctx.context.adapter.delete_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}])
265
- ctx.context.adapter.delete_many(model: "member", where: [{field: "organizationId", value: organization["id"]}])
266
- ctx.context.adapter.delete_many(model: "organizationRole", where: [{field: "organizationId", value: organization["id"]}]) if org_truthy?(config.dig(:dynamic_access_control, :enabled))
267
- ctx.context.adapter.delete(model: "organization", where: [{field: "id", value: organization["id"]}])
277
+ clear_active_organization_session(ctx, session, organization["id"])
278
+ run_org_hook(config, :after_delete_organization, {organization: organization_wire(ctx, organization), user: session[:user]}, ctx)
268
279
  ctx.json({status: true})
269
280
  end
270
281
  end
@@ -332,21 +343,8 @@ module BetterAuth
332
343
  end
333
344
  existing_member = find_member_by_email(ctx, organization["id"], email)
334
345
  raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION")) if existing_member
335
- pending = ctx.context.adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}, {field: "email", value: email}, {field: "status", value: "pending"}])
336
- if pending.any?
337
- if config[:cancel_pending_invitations_on_re_invite]
338
- pending.each { |entry| ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: entry["id"]}], update: {status: "canceled"}) }
339
- else
340
- raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION"))
341
- end
342
- end
343
- pending_count = ctx.context.adapter.count(model: "invitation", where: [{field: "organizationId", value: organization["id"]}, {field: "status", value: "pending"}])
344
- limit = config[:invitation_limit]
345
- if limit && pending_count >= limit.to_i
346
- raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_LIMIT_REACHED"))
347
- end
348
346
  team_ids = organization_team_ids(body[:team_id] || body[:team_ids])
349
- ensure_team_member_capacity!(ctx, config, team_ids)
347
+ validate_invitation_team_ids!(ctx, config, organization["id"], body[:team_id] || body[:team_ids], team_ids)
350
348
  invitation_data = {
351
349
  organizationId: organization["id"],
352
350
  email: email,
@@ -358,11 +356,25 @@ module BetterAuth
358
356
  createdAt: Time.now
359
357
  }.merge(additional_input(body, :organization_id, :organization_slug, :email, :role, :team_id, :team_ids))
360
358
  merge_hook_data!(invitation_data, run_org_hook(config, :before_create_invitation, {invitation: invitation_data, inviter: session[:user], organization: organization_wire(ctx, organization)}, ctx))
361
- invitation = ctx.context.adapter.create(
362
- model: "invitation",
363
- data: invitation_data,
364
- force_allow_id: true
365
- )
359
+ invitation = organization_transaction(ctx) do |adapter|
360
+ lock_organization_capacity!(adapter, organization)
361
+ pending = adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}, {field: "email", value: email}, {field: "status", value: "pending"}])
362
+ if pending.any?
363
+ if config[:cancel_pending_invitations_on_re_invite]
364
+ pending.each { |entry| adapter.update(model: "invitation", where: [{field: "id", value: entry["id"]}], update: {status: "canceled"}) }
365
+ else
366
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION"))
367
+ end
368
+ end
369
+ pending_count = adapter.count(model: "invitation", where: [{field: "organizationId", value: organization["id"]}, {field: "status", value: "pending"}])
370
+ limit = config[:invitation_limit]
371
+ if limit && pending_count >= limit.to_i
372
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_LIMIT_REACHED"))
373
+ end
374
+ validate_stored_invitation_teams!(adapter, organization["id"], team_ids)
375
+ ensure_team_member_capacity!(ctx, config, team_ids, adapter: adapter)
376
+ adapter.create(model: "invitation", data: invitation_data, force_allow_id: true)
377
+ end
366
378
  sender = config[:send_invitation_email]
367
379
  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)
368
380
  run_org_hook(config, :after_create_invitation, {invitation: invitation_wire(ctx, invitation), inviter: session[:user], organization: organization_wire(ctx, organization)}, ctx)
@@ -375,32 +387,56 @@ module BetterAuth
375
387
  session = Routes.current_session(ctx)
376
388
  body = normalize_hash(ctx.body)
377
389
  invitation = invitation_by_id(ctx, body[:invitation_id] || body[:id])
378
- raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
379
- raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION")) unless invitation["email"].to_s.downcase == session[:user]["email"].to_s.downcase
380
- raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation["status"] == "pending"
381
- raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) if invitation["expiresAt"] && Time.parse(invitation["expiresAt"].to_s) < Time.now
382
- if config[:require_email_verification_on_invitation] && !session[:user]["emailVerified"]
383
- raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION"))
384
- end
385
- ensure_team_member_capacity!(ctx, config, organization_team_ids(invitation["teamId"]))
386
- member = ctx.context.adapter.create(model: "member", data: {organizationId: invitation["organizationId"], userId: session[:user]["id"], role: invitation["role"], createdAt: Time.now})
387
- organization_team_ids(invitation["teamId"]).each do |team_id|
388
- ctx.context.adapter.create(model: "teamMember", data: {teamId: team_id, userId: session[:user]["id"], createdAt: Time.now})
389
- end
390
- updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "accepted"})
390
+ require_pending_invitation!(invitation)
391
+ require_invitation_recipient!(invitation, session)
392
+ require_verified_invitation_recipient!(ctx, config, session, "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION")
391
393
  organization = organization_by_id(ctx, invitation["organizationId"])
394
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
395
+
396
+ run_org_hook(config, :before_accept_invitation, {invitation: invitation_wire(ctx, invitation), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
397
+ member = nil
398
+ updated = nil
399
+ organization_transaction(ctx) do |adapter|
400
+ lock_organization_capacity!(adapter, organization)
401
+ accepted = adapter.increment_one(
402
+ model: "invitation",
403
+ where: [{field: "id", value: invitation["id"]}, {field: "status", value: "pending"}],
404
+ increment: {},
405
+ set: {status: "accepted"}
406
+ )
407
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless accepted
408
+
409
+ validate_organization_roles!(ctx, config, invitation["organizationId"], normalized_role_names(invitation["role"]), adapter: adapter)
410
+ ensure_organization_member_capacity!(ctx, config, session[:user], organization, adapter: adapter)
411
+ team_ids = organization_team_ids(invitation["teamId"])
412
+ validate_stored_invitation_teams!(adapter, invitation["organizationId"], team_ids)
413
+ ensure_team_member_capacity!(ctx, config, team_ids, adapter: adapter, lock: true)
414
+ if adapter.find_one(model: "member", where: [{field: "userId", value: session[:user]["id"]}, {field: "organizationId", value: invitation["organizationId"]}])
415
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION"))
416
+ end
417
+ member = adapter.create(model: "member", data: {organizationId: invitation["organizationId"], userId: session[:user]["id"], role: invitation["role"], createdAt: Time.now})
418
+ team_ids.each do |team_id|
419
+ adapter.create(model: "teamMember", data: {teamId: team_id, userId: session[:user]["id"], createdAt: Time.now})
420
+ end
421
+ updated = accepted
422
+ end
392
423
  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)
393
424
  ctx.json({invitation: invitation_wire(ctx, updated), member: member_wire(ctx, member)})
394
425
  end
395
426
  end
396
427
 
397
- def organization_reject_invitation_endpoint(_config)
428
+ def organization_reject_invitation_endpoint(config)
398
429
  Endpoint.new(path: "/organization/reject-invitation", method: "POST", metadata: organization_openapi("rejectOrganizationInvitation", "Reject an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
399
430
  session = Routes.current_session(ctx)
400
431
  invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
401
- raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
402
- raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION")) unless invitation["email"].to_s.downcase == session[:user]["email"].to_s.downcase
432
+ require_pending_invitation!(invitation, check_expiration: false)
433
+ require_invitation_recipient!(invitation, session)
434
+ require_verified_invitation_recipient!(ctx, config, session, "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION")
435
+ organization = organization_by_id(ctx, invitation["organizationId"])
436
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
437
+ run_org_hook(config, :before_reject_invitation, {invitation: invitation_wire(ctx, invitation), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
403
438
  updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "rejected"})
439
+ run_org_hook(config, :after_reject_invitation, {invitation: invitation_wire(ctx, updated), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
404
440
  ctx.json(invitation_wire(ctx, updated))
405
441
  end
406
442
  end
@@ -411,16 +447,28 @@ module BetterAuth
411
447
  invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
412
448
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
413
449
  require_org_permission!(ctx, config, session, invitation["organizationId"], {invitation: ["cancel"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION"))
450
+ organization = organization_by_id(ctx, invitation["organizationId"])
451
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
452
+ run_org_hook(config, :before_cancel_invitation, {invitation: invitation_wire(ctx, invitation), cancelled_by: session[:user], organization: organization_wire(ctx, organization)}, ctx)
414
453
  updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "canceled"})
454
+ run_org_hook(config, :after_cancel_invitation, {invitation: invitation_wire(ctx, updated), cancelled_by: session[:user], organization: organization_wire(ctx, organization)}, ctx)
415
455
  ctx.json(invitation_wire(ctx, updated))
416
456
  end
417
457
  end
418
458
 
419
- def organization_get_invitation_endpoint
459
+ def organization_get_invitation_endpoint(config)
420
460
  Endpoint.new(path: "/organization/get-invitation", method: "GET", metadata: organization_openapi("getOrganizationInvitation", "Get an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
461
+ session = Routes.current_session(ctx)
421
462
  invitation = invitation_by_id(ctx, normalize_hash(ctx.query)[:id] || normalize_hash(ctx.query)[:invitation_id])
422
- raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
423
- ctx.json(invitation_wire(ctx, invitation))
463
+ require_pending_invitation!(invitation)
464
+ require_invitation_recipient!(invitation, session)
465
+ require_verified_invitation_recipient!(ctx, config, session, "EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION")
466
+ organization = organization_by_id(ctx, invitation["organizationId"])
467
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
468
+ inviter = require_member(ctx, invitation["inviterId"], invitation["organizationId"])
469
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION")) unless inviter
470
+ inviter_user = ctx.context.internal_adapter.find_user_by_id(invitation["inviterId"])
471
+ ctx.json(invitation_wire(ctx, invitation).merge(organizationName: organization["name"], organizationSlug: organization["slug"], inviterEmail: inviter_user && inviter_user["email"]))
424
472
  end
425
473
  end
426
474
 
@@ -438,6 +486,9 @@ module BetterAuth
438
486
  def organization_list_user_invitations_endpoint
439
487
  Endpoint.new(path: "/organization/list-user-invitations", method: "GET", metadata: organization_openapi("listUserInvitations", "List user invitations", response: organization_array_schema("Invitation"))) do |ctx|
440
488
  session = Routes.current_session(ctx)
489
+ unless session[:user]["emailVerified"]
490
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION"))
491
+ end
441
492
  invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "email", value: session[:user]["email"].to_s.downcase}, {field: "status", value: "pending"}])
442
493
  ctx.json(invitations.map { |entry| invitation_wire(ctx, entry) })
443
494
  end
@@ -447,18 +498,47 @@ module BetterAuth
447
498
  Endpoint.new(path: "/organization/add-member", method: "POST", metadata: organization_openapi("addOrganizationMember", "Add an organization member", response: organization_ref_schema("Member"))) do |ctx|
448
499
  session = Routes.current_session(ctx)
449
500
  body = normalize_hash(ctx.body)
450
- organization_id = body[:organization_id]
501
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
502
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
503
+
504
+ organization = organization_by_id(ctx, organization_id)
505
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
506
+
451
507
  require_org_permission!(ctx, config, session, organization_id, {member: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_MEMBERSHIP_LIMIT_REACHED"))
452
508
  user_id = body[:user_id].to_s
453
509
  raise APIError.new("BAD_REQUEST", message: "userId is required") if user_id.empty?
510
+
511
+ user = ctx.context.internal_adapter.find_user_by_id(user_id)
512
+ raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless user
513
+
454
514
  if require_member(ctx, user_id, organization_id)
455
515
  raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION"))
456
516
  end
457
- organization = organization_by_id(ctx, organization_id)
458
- user = ctx.context.internal_adapter.find_user_by_id(user_id)
459
- member_data = {organizationId: organization_id, userId: user_id, role: parse_roles(body[:role] || "member"), createdAt: Time.now}.merge(additional_input(body, :organization_id, :user_id, :role))
517
+ team_id = body[:team_id]
518
+ if team_id
519
+ unless org_truthy?(config.dig(:teams, :enabled))
520
+ raise APIError.new("BAD_REQUEST", message: "Teams are not enabled")
521
+ end
522
+ team = team_by_id(ctx, team_id)
523
+ unless team && team["organizationId"] == organization_id
524
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND"))
525
+ end
526
+ end
527
+
528
+ member_data = {organizationId: organization_id, userId: user_id, role: parse_roles(body[:role] || "member"), createdAt: Time.now}.merge(additional_input(body, :organization_id, :user_id, :role, :team_id))
460
529
  merge_hook_data!(member_data, run_org_hook(config, :before_add_member, {member: member_data, user: user, organization: organization_wire(ctx, organization)}, ctx))
461
- member = ctx.context.adapter.create(model: "member", data: member_data)
530
+ member = organization_transaction(ctx) do |adapter|
531
+ lock_organization_capacity!(adapter, organization)
532
+ validate_organization_roles!(ctx, config, organization_id, normalized_role_names(member_data[:role]), adapter: adapter)
533
+ ensure_organization_member_capacity!(ctx, config, user, organization, adapter: adapter)
534
+ ensure_team_member_capacity!(ctx, config, [team_id], adapter: adapter, lock: true) if team_id
535
+ if adapter.find_one(model: "member", where: [{field: "userId", value: user_id}, {field: "organizationId", value: organization_id}])
536
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION"))
537
+ end
538
+ created = adapter.create(model: "member", data: member_data)
539
+ adapter.create(model: "teamMember", data: {teamId: team_id, userId: user_id, createdAt: Time.now}) if team_id
540
+ created
541
+ end
462
542
  run_org_hook(config, :after_add_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
463
543
  ctx.json(member_wire(ctx, member))
464
544
  end
@@ -471,11 +551,18 @@ module BetterAuth
471
551
  member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
472
552
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless member
473
553
  require_org_permission!(ctx, config, session, member["organizationId"], {member: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER"))
474
- ensure_not_last_owner!(ctx, member)
475
554
  organization = organization_by_id(ctx, member["organizationId"])
476
555
  user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
477
- ctx.context.adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
478
- ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "userId", value: member["userId"]}]) if org_truthy?(config.dig(:teams, :enabled))
556
+ run_org_hook(config, :before_remove_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
557
+ organization_transaction(ctx) do |adapter|
558
+ lock_organization_capacity!(adapter, organization)
559
+ ensure_not_last_owner!(ctx, member, adapter: adapter)
560
+ adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
561
+ if org_truthy?(config.dig(:teams, :enabled))
562
+ team_ids = adapter.find_many(model: "team", where: [{field: "organizationId", value: member["organizationId"]}]).map { |team| team["id"] }
563
+ adapter.delete_many(model: "teamMember", where: [{field: "userId", value: member["userId"]}, {field: "teamId", value: team_ids, operator: "in"}]) if team_ids.any?
564
+ end
565
+ end
479
566
  run_org_hook(config, :after_remove_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
480
567
  ctx.json({status: true})
481
568
  end
@@ -485,10 +572,57 @@ module BetterAuth
485
572
  Endpoint.new(path: "/organization/update-member-role", method: "POST", metadata: organization_openapi("updateOrganizationMemberRole", "Update an organization member role", response: organization_ref_schema("Member"))) do |ctx|
486
573
  session = Routes.current_session(ctx)
487
574
  body = normalize_hash(ctx.body)
488
- member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
575
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
576
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
577
+
578
+ role_names = normalized_role_names(body[:role])
579
+ raise APIError.new("BAD_REQUEST") if role_names.empty?
580
+
581
+ validate_organization_roles!(ctx, config, organization_id, role_names)
582
+
583
+ updating_member = require_member(ctx, session[:user]["id"], organization_id)
584
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless updating_member
585
+
586
+ member = if body[:member_id]
587
+ (body[:member_id].to_s == updating_member["id"]) ? updating_member : member_by_id(ctx, body[:member_id])
588
+ else
589
+ require_member(ctx, body[:user_id], organization_id)
590
+ end
489
591
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless member
490
- require_org_permission!(ctx, config, session, member["organizationId"], {member: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER"))
491
- updated = ctx.context.adapter.update(model: "member", where: [{field: "id", value: member["id"]}], update: {role: parse_roles(body[:role])})
592
+ unless member["organizationId"] == organization_id
593
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER"))
594
+ end
595
+
596
+ creator_role = config[:creator_role]
597
+ updating_member_roles = normalized_role_names(updating_member["role"])
598
+ member_roles = normalized_role_names(member["role"])
599
+ updater_is_creator = updating_member_roles.include?(creator_role)
600
+ updating_creator = member_roles.include?(creator_role)
601
+ setting_creator_role = role_names.include?(creator_role)
602
+
603
+ if (updating_creator && !updater_is_creator) || (setting_creator_role && !updater_is_creator)
604
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER"))
605
+ end
606
+
607
+ if updater_is_creator && updating_member["id"] == member["id"] && !setting_creator_role
608
+ creator_count = count_members_with_role(ctx, organization_id, creator_role)
609
+ if creator_count <= 1
610
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER"))
611
+ end
612
+ end
613
+
614
+ unless updater_is_creator
615
+ require_org_permission!(ctx, config, session, organization_id, {member: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER"))
616
+ end
617
+ organization = organization_by_id(ctx, organization_id)
618
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
619
+ user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
620
+ raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless user
621
+ previous_role = member["role"]
622
+ update = {role: parse_roles(role_names)}
623
+ merge_hook_data!(update, run_org_hook(config, :before_update_member_role, {member: member_wire(ctx, member), new_role: update[:role], user: user, organization: organization_wire(ctx, organization)}, ctx))
624
+ updated = ctx.context.adapter.update(model: "member", where: [{field: "id", value: member["id"]}], update: update)
625
+ run_org_hook(config, :after_update_member_role, {member: member_wire(ctx, updated), previous_role: previous_role, user: user, organization: organization_wire(ctx, organization)}, ctx)
492
626
  ctx.json(member_wire(ctx, updated))
493
627
  end
494
628
  end
@@ -520,9 +654,17 @@ module BetterAuth
520
654
  session = Routes.current_session(ctx)
521
655
  organization_id = normalize_hash(ctx.body)[:organization_id]
522
656
  member = require_member!(ctx, session[:user]["id"], organization_id)
523
- ensure_not_last_owner!(ctx, member)
524
- ctx.context.adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
525
- ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}]) if org_truthy?(config.dig(:teams, :enabled))
657
+ organization = organization_by_id(ctx, organization_id)
658
+ organization_transaction(ctx) do |adapter|
659
+ lock_organization_capacity!(adapter, organization)
660
+ ensure_not_last_owner!(ctx, member, adapter: adapter)
661
+ adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
662
+ if org_truthy?(config.dig(:teams, :enabled))
663
+ team_ids = adapter.find_many(model: "team", where: [{field: "organizationId", value: organization_id}]).map { |team| team["id"] }
664
+ adapter.delete_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}, {field: "teamId", value: team_ids, operator: "in"}]) if team_ids.any?
665
+ end
666
+ end
667
+ clear_active_organization_session(ctx, session, organization_id)
526
668
  ctx.json({status: true})
527
669
  end
528
670
  end
@@ -557,14 +699,18 @@ module BetterAuth
557
699
  organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
558
700
  require_org_permission!(ctx, config, session, organization_id, {team: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION"))
559
701
  organization = organization_by_id(ctx, organization_id)
560
- max_teams = config.dig(:teams, :maximum_teams)
561
- if max_teams && ctx.context.adapter.count(model: "team", where: [{field: "organizationId", value: organization_id}]) >= max_teams.to_i
562
- raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS"))
563
- end
564
702
  team_data = {organizationId: organization_id, name: body[:name].to_s, createdAt: Time.now}.merge(additional_input(body, :organization_id, :name))
565
703
  merge_hook_data!(team_data, run_org_hook(config, :before_create_team, {team: team_data, user: session[:user], organization: organization_wire(ctx, organization)}, ctx))
566
- team = ctx.context.adapter.create(model: "team", data: team_data, force_allow_id: true)
567
- ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
704
+ team = organization_transaction(ctx) do |adapter|
705
+ lock_organization_capacity!(adapter, organization)
706
+ max_teams = config.dig(:teams, :maximum_teams)
707
+ if max_teams && adapter.count(model: "team", where: [{field: "organizationId", value: organization_id}]) >= max_teams.to_i
708
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS"))
709
+ end
710
+ created = adapter.create(model: "team", data: team_data, force_allow_id: true)
711
+ adapter.create(model: "teamMember", data: {teamId: created["id"], userId: session[:user]["id"], createdAt: Time.now})
712
+ created
713
+ end
568
714
  run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
569
715
  ctx.json(team_wire(ctx, team))
570
716
  end
@@ -587,7 +733,11 @@ module BetterAuth
587
733
  team = team_by_id(ctx, body[:team_id])
588
734
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
589
735
  require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM"))
590
- updated = ctx.context.adapter.update(model: "team", where: [{field: "id", value: team["id"]}], update: additional_input(body, :team_id, :organization_id))
736
+ organization = organization_by_id(ctx, team["organizationId"])
737
+ update = additional_input(body[:data].is_a?(Hash) ? body[:data] : body, :team_id, :organization_id)
738
+ merge_hook_data!(update, run_org_hook(config, :before_update_team, {team: team_wire(ctx, team), updates: update, user: session[:user], organization: organization_wire(ctx, organization)}, ctx))
739
+ updated = ctx.context.adapter.update(model: "team", where: [{field: "id", value: team["id"]}], update: update)
740
+ run_org_hook(config, :after_update_team, {team: team_wire(ctx, updated), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
591
741
  ctx.json(team_wire(ctx, updated))
592
742
  end
593
743
  end
@@ -598,12 +748,26 @@ module BetterAuth
598
748
  team = team_by_id(ctx, normalize_hash(ctx.body)[:team_id])
599
749
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
600
750
  require_org_permission!(ctx, config, session, team["organizationId"], {team: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM"))
601
- teams = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: team["organizationId"]}])
602
- if teams.length <= 1 && config.dig(:teams, :allow_removing_all_teams) != true
603
- raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("UNABLE_TO_REMOVE_LAST_TEAM"))
751
+ organization = organization_by_id(ctx, team["organizationId"])
752
+ run_org_hook(config, :before_delete_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
753
+ organization_transaction(ctx) do |adapter|
754
+ lock_organization_capacity!(adapter, organization)
755
+ teams = adapter.find_many(model: "team", where: [{field: "organizationId", value: team["organizationId"]}])
756
+ if teams.length <= 1 && config.dig(:teams, :allow_removing_all_teams) != true
757
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("UNABLE_TO_REMOVE_LAST_TEAM"))
758
+ end
759
+ adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team["id"]}])
760
+ adapter.delete(model: "team", where: [{field: "id", value: team["id"]}])
761
+ pending = adapter.find_many(model: "invitation", where: [{field: "organizationId", value: team["organizationId"]}, {field: "status", value: "pending"}])
762
+ pending.each do |invitation|
763
+ team_ids = organization_team_ids(invitation["teamId"])
764
+ next unless team_ids.include?(team["id"])
765
+
766
+ remaining = team_ids.reject { |team_id| team_id == team["id"] }
767
+ adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {teamId: remaining.any? ? remaining.join(",") : nil})
768
+ end
604
769
  end
605
- ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team["id"]}])
606
- ctx.context.adapter.delete(model: "team", where: [{field: "id", value: team["id"]}])
770
+ run_org_hook(config, :after_delete_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
607
771
  ctx.json({status: true})
608
772
  end
609
773
  end
@@ -619,9 +783,14 @@ module BetterAuth
619
783
  end
620
784
  team = team_by_id(ctx, body[:team_id])
621
785
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
786
+ active_organization_id = session[:session]["activeOrganizationId"]
787
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless active_organization_id
788
+ unless team["organizationId"] == active_organization_id
789
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND"))
790
+ end
622
791
  require_team_member!(ctx, session[:user]["id"], team["id"])
623
- updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeOrganizationId: team["organizationId"], activeTeamId: team["id"]})
624
- Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeOrganizationId" => team["organizationId"], "activeTeamId" => team["id"]), user: session[:user]})
792
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeTeamId: team["id"]})
793
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeTeamId" => team["id"]), user: session[:user]})
625
794
  ctx.json(team_wire(ctx, team))
626
795
  end
627
796
  end
@@ -655,14 +824,18 @@ module BetterAuth
655
824
  require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER"))
656
825
  user_id = body[:user_id].to_s
657
826
  require_member!(ctx, user_id, team["organizationId"])
658
- max_members = config.dig(:teams, :maximum_members_per_team)
659
- if max_members && ctx.context.adapter.count(model: "teamMember", where: [{field: "teamId", value: team["id"]}]) >= max_members.to_i
660
- raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_MEMBER_LIMIT_REACHED"))
827
+ user = ctx.context.internal_adapter.find_user_by_id(user_id)
828
+ organization = organization_by_id(ctx, team["organizationId"])
829
+ member_data = {teamId: team["id"], userId: user_id, createdAt: Time.now}
830
+ run_org_hook(config, :before_add_team_member, {team_member: member_data, team: team_wire(ctx, team), user: user, organization: organization_wire(ctx, organization)}, ctx)
831
+ member = organization_transaction(ctx) do |adapter|
832
+ existing = adapter.find_one(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: user_id}])
833
+ next existing if existing
834
+
835
+ ensure_team_member_capacity!(ctx, config, [team["id"]], adapter: adapter, lock: true)
836
+ adapter.create(model: "teamMember", data: member_data)
661
837
  end
662
- existing = ctx.context.adapter.find_one(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: user_id}])
663
- next ctx.json(team_member_wire(ctx, existing)) if existing
664
-
665
- member = ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: user_id, createdAt: Time.now})
838
+ run_org_hook(config, :after_add_team_member, {team_member: team_member_wire(ctx, member), team: team_wire(ctx, team), user: user, organization: organization_wire(ctx, organization)}, ctx)
666
839
  ctx.json(team_member_wire(ctx, member))
667
840
  end
668
841
  end
@@ -674,7 +847,13 @@ module BetterAuth
674
847
  team = team_by_id(ctx, body[:team_id])
675
848
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
676
849
  require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER"))
850
+ member = ctx.context.adapter.find_one(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: body[:user_id]}])
851
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_NOT_A_MEMBER_OF_THE_TEAM")) unless member
852
+ user = ctx.context.internal_adapter.find_user_by_id(body[:user_id])
853
+ organization = organization_by_id(ctx, team["organizationId"])
854
+ run_org_hook(config, :before_remove_team_member, {team_member: team_member_wire(ctx, member), team: team_wire(ctx, team), user: user, organization: organization_wire(ctx, organization)}, ctx)
677
855
  ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: body[:user_id]}])
856
+ run_org_hook(config, :after_remove_team_member, {team_member: team_member_wire(ctx, member), team: team_wire(ctx, team), user: user, organization: organization_wire(ctx, organization)}, ctx)
678
857
  ctx.json({status: true})
679
858
  end
680
859
  end
@@ -689,7 +868,7 @@ module BetterAuth
689
868
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NAME_IS_ALREADY_TAKEN")) if organization_roles(config).key?(role_name) || organization_role_by_name(ctx, organization_id, role_name)
690
869
  permission = stringify_permission(body[:permission] || body[:permissions])
691
870
  validate_permission_resources!(config, permission)
692
- unless organization_permission?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
871
+ unless organization_can_delegate_permissions?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
693
872
  raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE"))
694
873
  end
695
874
  role = ctx.context.adapter.create(model: "organizationRole", data: {organizationId: organization_id, role: role_name, permission: JSON.generate(permission), createdAt: Time.now}.merge(additional_input(body, :organization_id, :role, :role_name, :permission, :permissions)))
@@ -735,7 +914,7 @@ module BetterAuth
735
914
  if permission
736
915
  permission = stringify_permission(permission)
737
916
  validate_permission_resources!(config, permission)
738
- unless organization_permission?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
917
+ unless organization_can_delegate_permissions?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
739
918
  raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE"))
740
919
  end
741
920
  update[:permission] = JSON.generate(permission)
@@ -794,53 +973,45 @@ module BetterAuth
794
973
  logo: string,
795
974
  metadata: object,
796
975
  userId: string,
797
- user_id: string,
798
- keepCurrentActiveOrganization: boolean,
799
- keep_current_active_organization: boolean
976
+ keepCurrentActiveOrganization: boolean
800
977
  },
801
978
  "checkOrganizationSlug" => {slug: string},
802
979
  "updateOrganization" => {
803
980
  organizationId: string,
804
- organization_id: string,
805
981
  organizationSlug: string,
806
- organization_slug: string,
807
982
  data: object,
808
983
  name: string,
809
984
  slug: string,
810
985
  logo: string,
811
986
  metadata: object
812
987
  },
813
- "deleteOrganization" => {organizationId: string, organization_id: string, organizationSlug: string, organization_slug: string},
814
- "setActiveOrganization" => {organizationId: string, organization_id: string, organizationSlug: string, organization_slug: string},
988
+ "deleteOrganization" => {organizationId: string, organizationSlug: string},
989
+ "setActiveOrganization" => {organizationId: string, organizationSlug: string},
815
990
  "createOrganizationInvitation" => {
816
991
  organizationId: string,
817
- organization_id: string,
818
992
  organizationSlug: string,
819
- organization_slug: string,
820
993
  email: {type: "string", format: "email"},
821
994
  role: {oneOf: [string, array.call]},
822
995
  teamId: string,
823
- team_id: string,
824
- teamIds: array.call,
825
- team_ids: array.call
996
+ teamIds: array.call
826
997
  },
827
- "acceptOrganizationInvitation" => {invitationId: string, invitation_id: string, id: string},
828
- "rejectOrganizationInvitation" => {invitationId: string, invitation_id: string},
829
- "cancelOrganizationInvitation" => {invitationId: string, invitation_id: string},
830
- "addOrganizationMember" => {organizationId: string, organization_id: string, userId: string, user_id: string, role: {oneOf: [string, array.call]}},
831
- "removeOrganizationMember" => {memberId: string, member_id: string, userId: string, user_id: string, organizationId: string, organization_id: string},
832
- "updateOrganizationMemberRole" => {memberId: string, member_id: string, userId: string, user_id: string, organizationId: string, organization_id: string, role: {oneOf: [string, array.call]}},
833
- "leaveOrganization" => {organizationId: string, organization_id: string},
834
- "hasOrganizationPermission" => {organizationId: string, organization_id: string, permission: object, permissions: object},
835
- "createOrganizationTeam" => {organizationId: string, organization_id: string, name: string},
836
- "updateOrganizationTeam" => {teamId: string, team_id: string, name: string},
837
- "removeOrganizationTeam" => {teamId: string, team_id: string},
838
- "setActiveOrganizationTeam" => {teamId: string, team_id: string},
839
- "addTeamMember" => {teamId: string, team_id: string, userId: string, user_id: string},
840
- "removeTeamMember" => {teamId: string, team_id: string, userId: string, user_id: string},
841
- "createOrganizationRole" => {organizationId: string, organization_id: string, role: string, roleName: string, role_name: string, permission: object, permissions: object},
842
- "updateOrganizationRole" => {organizationId: string, organization_id: string, roleId: string, role_id: string, role: string, roleName: string, role_name: string, permission: object, permissions: object, data: object},
843
- "deleteOrganizationRole" => {organizationId: string, organization_id: string, roleId: string, role_id: string, role: string, roleName: string, role_name: string}
998
+ "acceptOrganizationInvitation" => {invitationId: string, id: string},
999
+ "rejectOrganizationInvitation" => {invitationId: string},
1000
+ "cancelOrganizationInvitation" => {invitationId: string},
1001
+ "addOrganizationMember" => {organizationId: string, userId: string, role: {oneOf: [string, array.call]}},
1002
+ "removeOrganizationMember" => {memberId: string, userId: string, organizationId: string},
1003
+ "updateOrganizationMemberRole" => {memberId: string, userId: string, organizationId: string, role: {oneOf: [string, array.call]}},
1004
+ "leaveOrganization" => {organizationId: string},
1005
+ "hasOrganizationPermission" => {organizationId: string, permission: object, permissions: object},
1006
+ "createOrganizationTeam" => {organizationId: string, name: string},
1007
+ "updateOrganizationTeam" => {teamId: string, name: string},
1008
+ "removeOrganizationTeam" => {teamId: string},
1009
+ "setActiveOrganizationTeam" => {teamId: string},
1010
+ "addTeamMember" => {teamId: string, userId: string},
1011
+ "removeTeamMember" => {teamId: string, userId: string},
1012
+ "createOrganizationRole" => {organizationId: string, role: string, roleName: string, permission: object, permissions: object},
1013
+ "updateOrganizationRole" => {organizationId: string, roleId: string, role: string, roleName: string, permission: object, permissions: object, data: object},
1014
+ "deleteOrganizationRole" => {organizationId: string, roleId: string, role: string, roleName: string}
844
1015
  }[operation_id]
845
1016
  end
846
1017
 
@@ -932,6 +1103,35 @@ module BetterAuth
932
1103
  Array(roles).join(",")
933
1104
  end
934
1105
 
1106
+ def normalized_role_names(roles)
1107
+ Array(roles).flat_map { |role| role.to_s.split(",") }.map(&:strip).reject(&:empty?)
1108
+ end
1109
+
1110
+ def validate_organization_roles!(ctx, config, organization_id, role_names, adapter: ctx.context.adapter)
1111
+ valid_static_roles = (organization_default_roles(config).keys + organization_roles(config).keys).uniq
1112
+ unknown_roles = role_names.reject { |role| valid_static_roles.include?(role) }.uniq
1113
+ return if unknown_roles.empty?
1114
+
1115
+ if org_truthy?(config.dig(:dynamic_access_control, :enabled))
1116
+ found_roles = adapter.find_many(
1117
+ model: "organizationRole",
1118
+ where: [{field: "organizationId", value: organization_id}, {field: "role", value: unknown_roles, operator: "in"}]
1119
+ ).map { |role| role["role"] }
1120
+ unknown_roles -= found_roles
1121
+ end
1122
+ return if unknown_roles.empty?
1123
+
1124
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND"))
1125
+ end
1126
+
1127
+ def count_members_with_role(ctx, organization_id, role)
1128
+ count = 0
1129
+ organization_each_adapter_record(ctx.context.adapter, "member", where: [{field: "organizationId", value: organization_id}]) do |member|
1130
+ count += 1 if normalized_role_names(member["role"]).include?(role)
1131
+ end
1132
+ count
1133
+ end
1134
+
935
1135
  def organization_roles(config)
936
1136
  (config[:roles] || organization_default_roles(config)).transform_keys(&:to_s)
937
1137
  end
@@ -952,6 +1152,14 @@ module BetterAuth
952
1152
  end
953
1153
  end
954
1154
 
1155
+ def organization_can_delegate_permissions?(ctx, config, role_string, permissions, organization_id)
1156
+ permissions.all? do |resource, actions|
1157
+ actions.all? do |action|
1158
+ organization_permission?(ctx, config, role_string, {resource => [action]}, organization_id)
1159
+ end
1160
+ end
1161
+ end
1162
+
955
1163
  def require_org_permission!(ctx, config, session, organization_id, permissions, message)
956
1164
  member = require_member!(ctx, session[:user]["id"], organization_id)
957
1165
  return member if organization_permission?(ctx, config, member["role"], permissions, organization_id)
@@ -1066,9 +1274,23 @@ module BetterAuth
1066
1274
  [requested, default].min
1067
1275
  end
1068
1276
 
1277
+ def ensure_organization_member_capacity!(ctx, config, user, organization, adapter: ctx.context.adapter)
1278
+ limit = organization_membership_capacity_limit(ctx, config, user, organization)
1279
+ count = adapter.count(model: "member", where: [{field: "organizationId", value: organization["id"]}])
1280
+ return if count < limit
1281
+
1282
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_MEMBERSHIP_LIMIT_REACHED"))
1283
+ end
1284
+
1285
+ def organization_membership_capacity_limit(ctx, config, user, organization)
1286
+ configured = config[:membership_limit] || 100
1287
+ value = configured.respond_to?(:call) ? configured.call(user, organization_wire(ctx, organization)) : configured
1288
+ numeric_member_limit(value)
1289
+ end
1290
+
1069
1291
  def numeric_member_limit(value)
1070
1292
  return value.to_i if value.is_a?(Numeric)
1071
- return value.to_i if value.to_s.match?(/\A\d+\z/)
1293
+ return value.to_i if value.to_s.match?(/\A-?\d+\z/)
1072
1294
 
1073
1295
  100
1074
1296
  end
@@ -1082,12 +1304,14 @@ module BetterAuth
1082
1304
  end
1083
1305
  end
1084
1306
 
1085
- def ensure_team_member_capacity!(ctx, config, team_ids)
1307
+ def ensure_team_member_capacity!(ctx, config, team_ids, adapter: ctx.context.adapter, lock: false)
1086
1308
  max_members = config.dig(:teams, :maximum_members_per_team)
1087
1309
  return unless max_members && team_ids.any?
1088
1310
 
1089
1311
  team_ids.each do |team_id|
1090
- count = ctx.context.adapter.count(model: "teamMember", where: [{field: "teamId", value: team_id}])
1312
+ team = adapter.find_one(model: "team", where: [{field: "id", value: team_id}])
1313
+ lock_team_capacity!(adapter, team) if lock && team
1314
+ count = adapter.count(model: "teamMember", where: [{field: "teamId", value: team_id}])
1091
1315
  if count >= max_members.to_i
1092
1316
  raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_MEMBER_LIMIT_REACHED"))
1093
1317
  end
@@ -1123,16 +1347,24 @@ module BetterAuth
1123
1347
  role.merge("permission" => parse_permission(role["permission"]))
1124
1348
  end
1125
1349
 
1126
- def ensure_not_last_owner!(ctx, member)
1350
+ def ensure_not_last_owner!(ctx, member, adapter: ctx.context.adapter)
1127
1351
  return unless member["role"].to_s.split(",").include?("owner")
1128
1352
 
1129
1353
  owner_count = 0
1130
- organization_each_adapter_record(ctx.context.adapter, "member", where: [{field: "organizationId", value: member["organizationId"]}]) do |entry|
1354
+ organization_each_adapter_record(adapter, "member", where: [{field: "organizationId", value: member["organizationId"]}]) do |entry|
1131
1355
  owner_count += 1 if entry["role"].to_s.split(",").include?("owner")
1132
1356
  end
1133
1357
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER")) if owner_count <= 1
1134
1358
  end
1135
1359
 
1360
+ def clear_active_organization_session(ctx, session, organization_id)
1361
+ return unless session[:session]["activeOrganizationId"] == organization_id
1362
+
1363
+ update = {activeOrganizationId: nil, activeTeamId: nil}
1364
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], update)
1365
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeOrganizationId" => nil, "activeTeamId" => nil), user: session[:user]})
1366
+ end
1367
+
1136
1368
  def create_default_team(ctx, config, organization, session)
1137
1369
  custom = config.dig(:teams, :default_team, :custom_create_default_team)
1138
1370
  team_data = {organizationId: organization["id"], name: organization["name"], createdAt: Time.now}
@@ -1169,11 +1401,38 @@ module BetterAuth
1169
1401
  end
1170
1402
 
1171
1403
  def run_org_hook(config, key, data, ctx)
1172
- hooks = [config.dig(:organization_hooks, key), config.dig(:hooks, key)]
1173
- hooks.concat(ctx.context.options.plugins.filter_map { |plugin| plugin.dig(:options, :organization_hooks, key) || plugin.dig("options", "organizationHooks", key.to_s) }) if ctx&.context&.options
1404
+ hooks = org_hook_candidates(config, key)
1405
+ hooks.concat(ctx.context.options.plugins.flat_map { |plugin| org_hook_candidates(plugin_options(plugin), key) }) if ctx&.context&.options
1174
1406
  hooks.compact.uniq.filter_map { |hook| hook.call(data, ctx) if hook.respond_to?(:call) }.find { |response| response.is_a?(Hash) && normalize_hash(response).key?(:data) }
1175
1407
  end
1176
1408
 
1409
+ def org_hook_candidates(options, key)
1410
+ data = normalize_hash(options || {})
1411
+ camel_key = camelize_lower(key)
1412
+ candidates = [
1413
+ data.dig(:organization_hooks, key),
1414
+ data.dig(:organization_hooks, camel_key),
1415
+ data.dig(:hooks, key),
1416
+ data.dig(:hooks, camel_key)
1417
+ ]
1418
+ if options.respond_to?(:dig)
1419
+ candidates << options.dig("organizationHooks", camel_key)
1420
+ candidates << options.dig("organizationHooks", key.to_s)
1421
+ candidates << options.dig(:organizationHooks, camel_key)
1422
+ candidates << options.dig(:organizationHooks, key.to_s)
1423
+ end
1424
+ candidates
1425
+ end
1426
+
1427
+ def plugin_options(plugin)
1428
+ plugin.respond_to?(:dig) ? (plugin.dig(:options) || plugin.dig("options") || {}) : {}
1429
+ end
1430
+
1431
+ def camelize_lower(key)
1432
+ parts = key.to_s.split("_")
1433
+ ([parts.first] + parts.drop(1).map(&:capitalize)).join
1434
+ end
1435
+
1177
1436
  def merge_hook_data!(target, response)
1178
1437
  data = if response.is_a?(Hash)
1179
1438
  normalize_hash(response)[:data]
@@ -1219,6 +1478,80 @@ module BetterAuth
1219
1478
  Array(value).flat_map { |entry| entry.to_s.split(",") }.map(&:strip).reject(&:empty?)
1220
1479
  end
1221
1480
 
1481
+ def validate_invitation_team_ids!(ctx, config, organization_id, raw_value, team_ids)
1482
+ return if raw_value.nil?
1483
+ unless org_truthy?(config.dig(:teams, :enabled))
1484
+ raise APIError.new("BAD_REQUEST", message: "Teams are not enabled")
1485
+ end
1486
+ if Array(raw_value).any? { |team_id| team_id.to_s.include?(",") }
1487
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVALID_TEAM_ID"))
1488
+ end
1489
+ team_ids.each do |team_id|
1490
+ team = team_by_id(ctx, team_id)
1491
+ unless team && team["organizationId"] == organization_id
1492
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND"))
1493
+ end
1494
+ end
1495
+ end
1496
+
1497
+ def require_pending_invitation!(invitation, check_expiration: true)
1498
+ expired = invitation && invitation["expiresAt"] && organization_time(invitation["expiresAt"]) < Time.now
1499
+ valid = invitation && invitation["status"] == "pending"
1500
+ valid &&= !expired if check_expiration
1501
+ unless valid
1502
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND"))
1503
+ end
1504
+ invitation
1505
+ end
1506
+
1507
+ def require_invitation_recipient!(invitation, session)
1508
+ return if invitation["email"].to_s.downcase == session[:user]["email"].to_s.downcase
1509
+
1510
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION"))
1511
+ end
1512
+
1513
+ def require_verified_invitation_recipient!(ctx, config, session, error_code)
1514
+ return unless require_verified_email_for_invitation_id_action?(ctx, config)
1515
+ return if session[:user]["emailVerified"]
1516
+
1517
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch(error_code))
1518
+ end
1519
+
1520
+ def require_verified_email_for_invitation_id_action?(ctx, config)
1521
+ return org_truthy?(config[:require_email_verification_on_invitation]) if config.key?(:require_email_verification_on_invitation)
1522
+
1523
+ generator = ctx.context.options.advanced.dig(:database, :generate_id)
1524
+ !generator.nil? && generator != "uuid"
1525
+ end
1526
+
1527
+ def organization_time(value)
1528
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
1529
+ end
1530
+
1531
+ def organization_transaction(ctx)
1532
+ adapter = ctx.context.adapter
1533
+ if adapter.method(:transaction).owner == BetterAuth::Adapters::Base
1534
+ raise BetterAuth::Error, "Organization mutations require an adapter with real transaction support"
1535
+ end
1536
+
1537
+ adapter.transaction { |transaction_adapter| yield(transaction_adapter || adapter) }
1538
+ end
1539
+
1540
+ def lock_organization_capacity!(adapter, organization)
1541
+ adapter.update(model: "organization", where: [{field: "id", value: organization["id"]}], update: {name: organization["name"]})
1542
+ end
1543
+
1544
+ def lock_team_capacity!(adapter, team)
1545
+ adapter.update(model: "team", where: [{field: "id", value: team["id"]}], update: {name: team["name"]})
1546
+ end
1547
+
1548
+ def validate_stored_invitation_teams!(adapter, organization_id, team_ids)
1549
+ team_ids.each do |team_id|
1550
+ team = adapter.find_one(model: "team", where: [{field: "id", value: team_id}, {field: "organizationId", value: organization_id}])
1551
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
1552
+ end
1553
+ end
1554
+
1222
1555
  def additional_input(hash, *exclude)
1223
1556
  data = normalize_hash(hash)
1224
1557
  additional = normalize_hash(data.delete(:additional_fields))