better_auth 0.8.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 +92 -0
- data/LICENSE.md +1 -1
- data/README.md +142 -95
- 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 +303 -54
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +390 -63
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +48 -47
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +119 -15
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +108 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/http_client.rb +46 -0
- 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 +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- 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 +85 -29
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- 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 +77 -5
- data/lib/better_auth/plugins/dub.rb +10 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +162 -140
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +44 -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 +233 -46
- 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 +78 -65
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +77 -9
- data/lib/better_auth/plugins/organization.rb +546 -102
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +25 -27
- 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 +205 -19
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +300 -133
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +44 -8
- data/lib/better_auth/routes/email_verification.rb +93 -6
- data/lib/better_auth/routes/password.rb +6 -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 +197 -54
- data/lib/better_auth/routes/user.rb +17 -10
- data/lib/better_auth/schema/sql.rb +189 -34
- data/lib/better_auth/schema.rb +159 -5
- data/lib/better_auth/session.rb +29 -10
- 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 +45 -23
- 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/github.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 +566 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +14 -41
- metadata +14 -18
- 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 -215
- data/lib/better_auth/plugins/oidc_provider.rb +0 -710
|
@@ -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,21 +654,29 @@ 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
|
|
529
671
|
|
|
530
|
-
def organization_list_members_endpoint(
|
|
672
|
+
def organization_list_members_endpoint(config)
|
|
531
673
|
Endpoint.new(path: "/organization/list-members", method: "GET", metadata: organization_openapi("listOrganizationMembers", "List organization members", response: organization_members_response_schema)) do |ctx|
|
|
532
674
|
session = Routes.current_session(ctx)
|
|
533
675
|
query = normalize_hash(ctx.query)
|
|
534
676
|
organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id") || session[:session]["activeOrganizationId"]
|
|
535
677
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
|
|
536
678
|
require_member!(ctx, session[:user]["id"], organization_id)
|
|
537
|
-
ctx.json(list_members_for(ctx, organization_id, query))
|
|
679
|
+
ctx.json(list_members_for(ctx, organization_id, query, config, session[:user]))
|
|
538
680
|
end
|
|
539
681
|
end
|
|
540
682
|
|
|
@@ -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)
|
|
@@ -768,6 +947,7 @@ module BetterAuth
|
|
|
768
947
|
end
|
|
769
948
|
|
|
770
949
|
def organization_openapi(operation_id, description, response:, response_description: "Success", request: nil, required: [], parameters: nil)
|
|
950
|
+
request ||= organization_request_schema(operation_id)
|
|
771
951
|
openapi = {
|
|
772
952
|
operationId: operation_id,
|
|
773
953
|
description: description,
|
|
@@ -781,6 +961,60 @@ module BetterAuth
|
|
|
781
961
|
{openapi: openapi}
|
|
782
962
|
end
|
|
783
963
|
|
|
964
|
+
def organization_request_schema(operation_id)
|
|
965
|
+
string = {type: "string"}
|
|
966
|
+
boolean = {type: "boolean"}
|
|
967
|
+
array = ->(items = string) { {type: "array", items: items} }
|
|
968
|
+
object = {type: "object", additionalProperties: true}
|
|
969
|
+
{
|
|
970
|
+
"createOrganization" => {
|
|
971
|
+
name: string,
|
|
972
|
+
slug: string,
|
|
973
|
+
logo: string,
|
|
974
|
+
metadata: object,
|
|
975
|
+
userId: string,
|
|
976
|
+
keepCurrentActiveOrganization: boolean
|
|
977
|
+
},
|
|
978
|
+
"checkOrganizationSlug" => {slug: string},
|
|
979
|
+
"updateOrganization" => {
|
|
980
|
+
organizationId: string,
|
|
981
|
+
organizationSlug: string,
|
|
982
|
+
data: object,
|
|
983
|
+
name: string,
|
|
984
|
+
slug: string,
|
|
985
|
+
logo: string,
|
|
986
|
+
metadata: object
|
|
987
|
+
},
|
|
988
|
+
"deleteOrganization" => {organizationId: string, organizationSlug: string},
|
|
989
|
+
"setActiveOrganization" => {organizationId: string, organizationSlug: string},
|
|
990
|
+
"createOrganizationInvitation" => {
|
|
991
|
+
organizationId: string,
|
|
992
|
+
organizationSlug: string,
|
|
993
|
+
email: {type: "string", format: "email"},
|
|
994
|
+
role: {oneOf: [string, array.call]},
|
|
995
|
+
teamId: string,
|
|
996
|
+
teamIds: array.call
|
|
997
|
+
},
|
|
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}
|
|
1015
|
+
}[operation_id]
|
|
1016
|
+
end
|
|
1017
|
+
|
|
784
1018
|
def organization_ref_schema(name)
|
|
785
1019
|
{
|
|
786
1020
|
type: "object",
|
|
@@ -869,6 +1103,35 @@ module BetterAuth
|
|
|
869
1103
|
Array(roles).join(",")
|
|
870
1104
|
end
|
|
871
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
|
+
|
|
872
1135
|
def organization_roles(config)
|
|
873
1136
|
(config[:roles] || organization_default_roles(config)).transform_keys(&:to_s)
|
|
874
1137
|
end
|
|
@@ -889,6 +1152,14 @@ module BetterAuth
|
|
|
889
1152
|
end
|
|
890
1153
|
end
|
|
891
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
|
+
|
|
892
1163
|
def require_org_permission!(ctx, config, session, organization_id, permissions, message)
|
|
893
1164
|
member = require_member!(ctx, session[:user]["id"], organization_id)
|
|
894
1165
|
return member if organization_permission?(ctx, config, member["role"], permissions, organization_id)
|
|
@@ -969,7 +1240,7 @@ module BetterAuth
|
|
|
969
1240
|
ctx.context.adapter.find_one(model: "organizationRole", where: [{field: "organizationId", value: organization_id}, {field: "role", value: role}])
|
|
970
1241
|
end
|
|
971
1242
|
|
|
972
|
-
def list_members_for(ctx, organization_id, query = {})
|
|
1243
|
+
def list_members_for(ctx, organization_id, query = {}, config = nil, user = nil)
|
|
973
1244
|
where = [{field: "organizationId", value: organization_id}]
|
|
974
1245
|
if query[:filter_field]
|
|
975
1246
|
where << {field: query[:filter_field], value: query[:filter_value], operator: query[:filter_operator]}
|
|
@@ -977,34 +1248,79 @@ module BetterAuth
|
|
|
977
1248
|
filter = normalize_hash(query[:filter])
|
|
978
1249
|
where << {field: filter[:field], value: filter[:value], operator: filter[:operator]}
|
|
979
1250
|
end
|
|
1251
|
+
limit = member_list_limit(ctx, organization_id, query, config, user)
|
|
980
1252
|
members = ctx.context.adapter.find_many(
|
|
981
1253
|
model: "member",
|
|
982
1254
|
where: where,
|
|
983
|
-
limit:
|
|
1255
|
+
limit: limit,
|
|
984
1256
|
offset: query[:offset],
|
|
985
1257
|
sort_by: query[:sort_by] ? {field: query[:sort_by], direction: query[:sort_direction] || query[:sort_order] || "asc"} : nil
|
|
986
1258
|
)
|
|
1259
|
+
users_by_id = member_users_by_id(ctx, members)
|
|
987
1260
|
{
|
|
988
|
-
members: members.map { |entry| member_wire(ctx, entry) },
|
|
1261
|
+
members: members.map { |entry| member_wire(ctx, entry, users_by_id: users_by_id) },
|
|
989
1262
|
total: ctx.context.adapter.count(model: "member", where: where)
|
|
990
1263
|
}
|
|
991
1264
|
end
|
|
992
1265
|
|
|
993
|
-
def
|
|
1266
|
+
def member_list_limit(ctx, organization_id, query, config, user)
|
|
1267
|
+
configured = config && config[:membership_limit]
|
|
1268
|
+
configured = 100 if configured.nil?
|
|
1269
|
+
default = numeric_member_limit(configured)
|
|
1270
|
+
default = 100 unless default.positive?
|
|
1271
|
+
requested = query[:limit].to_i if query.key?(:limit) && !query[:limit].to_s.empty?
|
|
1272
|
+
return default unless requested&.positive?
|
|
1273
|
+
|
|
1274
|
+
[requested, default].min
|
|
1275
|
+
end
|
|
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
|
+
|
|
1291
|
+
def numeric_member_limit(value)
|
|
1292
|
+
return value.to_i if value.is_a?(Numeric)
|
|
1293
|
+
return value.to_i if value.to_s.match?(/\A-?\d+\z/)
|
|
1294
|
+
|
|
1295
|
+
100
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
def member_users_by_id(ctx, members)
|
|
1299
|
+
user_ids = members.map { |member| member["userId"] }.compact.uniq
|
|
1300
|
+
return {} if user_ids.empty?
|
|
1301
|
+
|
|
1302
|
+
ctx.context.adapter.find_many(model: "user", where: [{field: "id", operator: "in", value: user_ids}]).each_with_object({}) do |user, result|
|
|
1303
|
+
result[user["id"]] = user
|
|
1304
|
+
end
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
def ensure_team_member_capacity!(ctx, config, team_ids, adapter: ctx.context.adapter, lock: false)
|
|
994
1308
|
max_members = config.dig(:teams, :maximum_members_per_team)
|
|
995
1309
|
return unless max_members && team_ids.any?
|
|
996
1310
|
|
|
997
1311
|
team_ids.each do |team_id|
|
|
998
|
-
|
|
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}])
|
|
999
1315
|
if count >= max_members.to_i
|
|
1000
1316
|
raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_MEMBER_LIMIT_REACHED"))
|
|
1001
1317
|
end
|
|
1002
1318
|
end
|
|
1003
1319
|
end
|
|
1004
1320
|
|
|
1005
|
-
def member_wire(ctx, member)
|
|
1321
|
+
def member_wire(ctx, member, users_by_id: nil)
|
|
1006
1322
|
data = Schema.parse_output(ctx.context.options, "member", member)
|
|
1007
|
-
user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
|
|
1323
|
+
user = users_by_id ? users_by_id[member["userId"]] : ctx.context.internal_adapter.find_user_by_id(member["userId"])
|
|
1008
1324
|
data["user"] = user.slice("id", "name", "email", "image") if user
|
|
1009
1325
|
data
|
|
1010
1326
|
end
|
|
@@ -1031,11 +1347,22 @@ module BetterAuth
|
|
|
1031
1347
|
role.merge("permission" => parse_permission(role["permission"]))
|
|
1032
1348
|
end
|
|
1033
1349
|
|
|
1034
|
-
def ensure_not_last_owner!(ctx, member)
|
|
1350
|
+
def ensure_not_last_owner!(ctx, member, adapter: ctx.context.adapter)
|
|
1035
1351
|
return unless member["role"].to_s.split(",").include?("owner")
|
|
1036
1352
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1353
|
+
owner_count = 0
|
|
1354
|
+
organization_each_adapter_record(adapter, "member", where: [{field: "organizationId", value: member["organizationId"]}]) do |entry|
|
|
1355
|
+
owner_count += 1 if entry["role"].to_s.split(",").include?("owner")
|
|
1356
|
+
end
|
|
1357
|
+
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER")) if owner_count <= 1
|
|
1358
|
+
end
|
|
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]})
|
|
1039
1366
|
end
|
|
1040
1367
|
|
|
1041
1368
|
def create_default_team(ctx, config, organization, session)
|
|
@@ -1053,16 +1380,59 @@ module BetterAuth
|
|
|
1053
1380
|
end
|
|
1054
1381
|
|
|
1055
1382
|
def organization_created_count(ctx, user_id)
|
|
1056
|
-
|
|
1057
|
-
|
|
1383
|
+
count = 0
|
|
1384
|
+
organization_each_adapter_record(ctx.context.adapter, "member", where: [{field: "userId", value: user_id}]) do |member|
|
|
1385
|
+
count += 1 if member["role"].to_s.split(",").include?("owner")
|
|
1386
|
+
end
|
|
1387
|
+
count
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
def organization_each_adapter_record(adapter, model, where:, page_size: 100)
|
|
1391
|
+
offset = 0
|
|
1392
|
+
loop do
|
|
1393
|
+
records = adapter.find_many(model: model, where: where, limit: page_size, offset: offset)
|
|
1394
|
+
break if records.empty?
|
|
1395
|
+
|
|
1396
|
+
records.each { |record| yield record }
|
|
1397
|
+
break if records.length < page_size
|
|
1398
|
+
|
|
1399
|
+
offset += records.length
|
|
1400
|
+
end
|
|
1058
1401
|
end
|
|
1059
1402
|
|
|
1060
1403
|
def run_org_hook(config, key, data, ctx)
|
|
1061
|
-
hooks =
|
|
1062
|
-
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
|
|
1063
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) }
|
|
1064
1407
|
end
|
|
1065
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
|
+
|
|
1066
1436
|
def merge_hook_data!(target, response)
|
|
1067
1437
|
data = if response.is_a?(Hash)
|
|
1068
1438
|
normalize_hash(response)[:data]
|
|
@@ -1108,6 +1478,80 @@ module BetterAuth
|
|
|
1108
1478
|
Array(value).flat_map { |entry| entry.to_s.split(",") }.map(&:strip).reject(&:empty?)
|
|
1109
1479
|
end
|
|
1110
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
|
+
|
|
1111
1555
|
def additional_input(hash, *exclude)
|
|
1112
1556
|
data = normalize_hash(hash)
|
|
1113
1557
|
additional = normalize_hash(data.delete(:additional_fields))
|