better_auth 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +4 -4
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +79 -28
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +142 -32
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +41 -14
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
|
@@ -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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
265
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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(
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
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
|
-
|
|
478
|
-
ctx
|
|
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
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
524
|
-
ctx
|
|
525
|
-
|
|
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
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
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"], {
|
|
624
|
-
Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
814
|
-
"setActiveOrganization" => {organizationId: 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
|
-
|
|
824
|
-
teamIds: array.call,
|
|
825
|
-
team_ids: array.call
|
|
996
|
+
teamIds: array.call
|
|
826
997
|
},
|
|
827
|
-
"acceptOrganizationInvitation" => {invitationId: string,
|
|
828
|
-
"rejectOrganizationInvitation" => {invitationId: string
|
|
829
|
-
"cancelOrganizationInvitation" => {invitationId: string
|
|
830
|
-
"addOrganizationMember" => {organizationId: string,
|
|
831
|
-
"removeOrganizationMember" => {memberId: string,
|
|
832
|
-
"updateOrganizationMemberRole" => {memberId: string,
|
|
833
|
-
"leaveOrganization" => {organizationId: string
|
|
834
|
-
"hasOrganizationPermission" => {organizationId: string,
|
|
835
|
-
"createOrganizationTeam" => {organizationId: string,
|
|
836
|
-
"updateOrganizationTeam" => {teamId: string,
|
|
837
|
-
"removeOrganizationTeam" => {teamId: string
|
|
838
|
-
"setActiveOrganizationTeam" => {teamId: string
|
|
839
|
-
"addTeamMember" => {teamId: string,
|
|
840
|
-
"removeTeamMember" => {teamId: string,
|
|
841
|
-
"createOrganizationRole" => {organizationId: string,
|
|
842
|
-
"updateOrganizationRole" => {organizationId: string,
|
|
843
|
-
"deleteOrganizationRole" => {organizationId: 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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
1173
|
-
hooks.concat(ctx.context.options.plugins.
|
|
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))
|