better_auth 0.3.0 → 0.5.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 +17 -0
- data/README.md +24 -0
- data/lib/better_auth/adapters/internal_adapter.rb +10 -7
- data/lib/better_auth/adapters/memory.rb +57 -11
- data/lib/better_auth/adapters/sql.rb +123 -20
- data/lib/better_auth/api.rb +114 -9
- data/lib/better_auth/async.rb +70 -0
- data/lib/better_auth/configuration.rb +97 -7
- data/lib/better_auth/context.rb +165 -12
- data/lib/better_auth/cookies.rb +6 -4
- data/lib/better_auth/core.rb +2 -0
- data/lib/better_auth/crypto/jwe.rb +27 -5
- data/lib/better_auth/crypto.rb +32 -0
- data/lib/better_auth/database_hooks.rb +8 -8
- data/lib/better_auth/deprecate.rb +28 -0
- data/lib/better_auth/endpoint.rb +92 -5
- data/lib/better_auth/error.rb +8 -1
- data/lib/better_auth/host.rb +166 -0
- data/lib/better_auth/instrumentation.rb +74 -0
- data/lib/better_auth/logger.rb +31 -0
- data/lib/better_auth/middleware/origin_check.rb +2 -2
- data/lib/better_auth/oauth2.rb +94 -0
- data/lib/better_auth/plugins/admin/schema.rb +2 -2
- data/lib/better_auth/plugins/admin.rb +344 -16
- data/lib/better_auth/plugins/anonymous.rb +37 -3
- data/lib/better_auth/plugins/device_authorization.rb +102 -5
- data/lib/better_auth/plugins/dub.rb +148 -0
- data/lib/better_auth/plugins/email_otp.rb +261 -19
- data/lib/better_auth/plugins/expo.rb +17 -1
- data/lib/better_auth/plugins/generic_oauth.rb +67 -35
- data/lib/better_auth/plugins/jwt.rb +37 -4
- data/lib/better_auth/plugins/last_login_method.rb +2 -2
- data/lib/better_auth/plugins/magic_link.rb +66 -3
- data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
- data/lib/better_auth/plugins/mcp/config.rb +51 -0
- data/lib/better_auth/plugins/mcp/consent.rb +31 -0
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
- data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
- data/lib/better_auth/plugins/mcp/registration.rb +31 -0
- data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
- data/lib/better_auth/plugins/mcp/schema.rb +91 -0
- data/lib/better_auth/plugins/mcp/token.rb +108 -0
- data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
- data/lib/better_auth/plugins/mcp.rb +111 -263
- data/lib/better_auth/plugins/multi_session.rb +61 -3
- data/lib/better_auth/plugins/oauth_protocol.rb +173 -30
- data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
- data/lib/better_auth/plugins/oidc_provider.rb +118 -14
- data/lib/better_auth/plugins/one_tap.rb +7 -2
- data/lib/better_auth/plugins/one_time_token.rb +42 -2
- data/lib/better_auth/plugins/open_api.rb +163 -318
- data/lib/better_auth/plugins/organization/schema.rb +6 -0
- data/lib/better_auth/plugins/organization.rb +186 -56
- data/lib/better_auth/plugins/phone_number.rb +141 -6
- data/lib/better_auth/plugins/siwe.rb +69 -3
- data/lib/better_auth/plugins/two_factor.rb +118 -41
- data/lib/better_auth/plugins/username.rb +57 -2
- data/lib/better_auth/rate_limiter.rb +38 -0
- data/lib/better_auth/request_state.rb +44 -0
- data/lib/better_auth/response.rb +42 -0
- data/lib/better_auth/router.rb +7 -1
- data/lib/better_auth/routes/account.rb +220 -42
- data/lib/better_auth/routes/email_verification.rb +98 -14
- data/lib/better_auth/routes/password.rb +126 -8
- data/lib/better_auth/routes/session.rb +128 -13
- data/lib/better_auth/routes/sign_in.rb +26 -2
- data/lib/better_auth/routes/sign_out.rb +13 -1
- data/lib/better_auth/routes/sign_up.rb +70 -4
- data/lib/better_auth/routes/social.rb +132 -7
- data/lib/better_auth/routes/user.rb +228 -20
- data/lib/better_auth/routes/validation.rb +50 -0
- data/lib/better_auth/secret_config.rb +115 -0
- data/lib/better_auth/session.rb +13 -2
- data/lib/better_auth/url_helpers.rb +206 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +12 -0
- metadata +23 -1
|
@@ -156,7 +156,7 @@ module BetterAuth
|
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
def organization_create_endpoint(config)
|
|
159
|
-
Endpoint.new(path: "/organization/create", method: "POST") do |ctx|
|
|
159
|
+
Endpoint.new(path: "/organization/create", method: "POST", metadata: organization_openapi("createOrganization", "Create an organization", response: organization_ref_schema("Organization"))) do |ctx|
|
|
160
160
|
body = normalize_hash(ctx.body)
|
|
161
161
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
162
162
|
user = session ? session[:user] : ctx.context.internal_adapter.find_user_by_id(body[:user_id])
|
|
@@ -204,7 +204,8 @@ module BetterAuth
|
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
def organization_check_slug_endpoint
|
|
207
|
-
Endpoint.new(path: "/organization/check-slug", method: "POST") do |ctx|
|
|
207
|
+
Endpoint.new(path: "/organization/check-slug", method: "POST", metadata: organization_openapi("checkOrganizationSlug", "Check if an organization slug is available", response: OpenAPI.status_response_schema)) do |ctx|
|
|
208
|
+
Routes.request_only_session(ctx)
|
|
208
209
|
slug = normalize_hash(ctx.body)[:slug].to_s
|
|
209
210
|
if slug.empty? || organization_by_slug(ctx, slug)
|
|
210
211
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_SLUG_ALREADY_TAKEN"))
|
|
@@ -214,7 +215,7 @@ module BetterAuth
|
|
|
214
215
|
end
|
|
215
216
|
|
|
216
217
|
def organization_list_endpoint
|
|
217
|
-
Endpoint.new(path: "/organization/list", method: "GET") do |ctx|
|
|
218
|
+
Endpoint.new(path: "/organization/list", method: "GET", metadata: organization_openapi("listOrganizations", "List organizations", response: organization_array_schema("Organization"))) do |ctx|
|
|
218
219
|
session = Routes.current_session(ctx)
|
|
219
220
|
members = ctx.context.adapter.find_many(model: "member", where: [{field: "userId", value: session[:user]["id"]}])
|
|
220
221
|
organizations = members.filter_map { |member| organization_by_id(ctx, member["organizationId"]) }
|
|
@@ -223,7 +224,7 @@ module BetterAuth
|
|
|
223
224
|
end
|
|
224
225
|
|
|
225
226
|
def organization_update_endpoint(config)
|
|
226
|
-
Endpoint.new(path: "/organization/update", method: "POST") do |ctx|
|
|
227
|
+
Endpoint.new(path: "/organization/update", method: "POST", metadata: organization_openapi("updateOrganization", "Update an organization", response: organization_ref_schema("Organization"))) do |ctx|
|
|
227
228
|
session = Routes.current_session(ctx)
|
|
228
229
|
body = normalize_hash(ctx.body)
|
|
229
230
|
id = body[:organization_id] || body[:organizationId]
|
|
@@ -248,7 +249,7 @@ module BetterAuth
|
|
|
248
249
|
end
|
|
249
250
|
|
|
250
251
|
def organization_delete_endpoint(config)
|
|
251
|
-
Endpoint.new(path: "/organization/delete", method: "POST") do |ctx|
|
|
252
|
+
Endpoint.new(path: "/organization/delete", method: "POST", metadata: organization_openapi("deleteOrganization", "Delete an organization", response: OpenAPI.status_response_schema)) do |ctx|
|
|
252
253
|
session = Routes.current_session(ctx)
|
|
253
254
|
body = normalize_hash(ctx.body)
|
|
254
255
|
organization = organization_by_id(ctx, body[:organization_id]) || organization_by_slug(ctx, body[:organization_slug])
|
|
@@ -269,9 +270,15 @@ module BetterAuth
|
|
|
269
270
|
end
|
|
270
271
|
|
|
271
272
|
def organization_set_active_endpoint
|
|
272
|
-
Endpoint.new(path: "/organization/set-active", method: "POST") do |ctx|
|
|
273
|
+
Endpoint.new(path: "/organization/set-active", method: "POST", metadata: organization_openapi("setActiveOrganization", "Set the active organization", response: organization_nullable_schema("Organization"))) do |ctx|
|
|
273
274
|
session = Routes.current_session(ctx, sensitive: true)
|
|
274
275
|
body = normalize_hash(ctx.body)
|
|
276
|
+
if body.key?(:organization_id) && body[:organization_id].nil?
|
|
277
|
+
updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeOrganizationId: nil, activeTeamId: nil})
|
|
278
|
+
Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeOrganizationId" => nil, "activeTeamId" => nil), user: session[:user]})
|
|
279
|
+
next ctx.json(nil)
|
|
280
|
+
end
|
|
281
|
+
|
|
275
282
|
organization = organization_by_id(ctx, body[:organization_id]) || organization_by_slug(ctx, body[:organization_slug])
|
|
276
283
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
|
|
277
284
|
require_member!(ctx, session[:user]["id"], organization["id"])
|
|
@@ -282,14 +289,19 @@ module BetterAuth
|
|
|
282
289
|
end
|
|
283
290
|
|
|
284
291
|
def organization_get_full_endpoint(config)
|
|
285
|
-
Endpoint.new(path: "/organization/get-full-organization", method: "GET") do |ctx|
|
|
292
|
+
Endpoint.new(path: "/organization/get-full-organization", method: "GET", metadata: organization_openapi("getOrganization", "Get the full organization", response: organization_nullable_schema("Organization"))) do |ctx|
|
|
286
293
|
session = Routes.current_session(ctx)
|
|
287
294
|
query = normalize_hash(ctx.query)
|
|
295
|
+
explicit_lookup = query.key?(:organization_slug) || query.key?(:organization_id)
|
|
288
296
|
organization = organization_by_slug(ctx, query[:organization_slug]) || organization_by_id(ctx, query[:organization_id] || session[:session]["activeOrganizationId"])
|
|
289
|
-
|
|
297
|
+
unless organization
|
|
298
|
+
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) if explicit_lookup
|
|
299
|
+
|
|
300
|
+
next ctx.json(nil)
|
|
301
|
+
end
|
|
290
302
|
|
|
291
303
|
require_member!(ctx, session[:user]["id"], organization["id"])
|
|
292
|
-
members = list_members_for(ctx, organization["id"])
|
|
304
|
+
members = list_members_for(ctx, organization["id"], {limit: query[:members_limit] || config[:membership_limit]})
|
|
293
305
|
invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}])
|
|
294
306
|
result = organization_wire(ctx, organization).merge(
|
|
295
307
|
members: members.fetch(:members),
|
|
@@ -304,10 +316,10 @@ module BetterAuth
|
|
|
304
316
|
end
|
|
305
317
|
|
|
306
318
|
def organization_invite_endpoint(config)
|
|
307
|
-
Endpoint.new(path: "/organization/invite-member", method: "POST") do |ctx|
|
|
319
|
+
Endpoint.new(path: "/organization/invite-member", method: "POST", metadata: organization_openapi("createOrganizationInvitation", "Create an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
|
|
308
320
|
session = Routes.current_session(ctx)
|
|
309
321
|
body = normalize_hash(ctx.body)
|
|
310
|
-
organization = organization_by_id(ctx, body[:organization_id])
|
|
322
|
+
organization = organization_by_id(ctx, body[:organization_id] || session[:session]["activeOrganizationId"]) || organization_by_slug(ctx, body[:organization_slug])
|
|
311
323
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
|
|
312
324
|
require_org_permission!(ctx, config, session, organization["id"], {invitation: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION"))
|
|
313
325
|
email = body[:email].to_s.downcase
|
|
@@ -334,27 +346,32 @@ module BetterAuth
|
|
|
334
346
|
raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_LIMIT_REACHED"))
|
|
335
347
|
end
|
|
336
348
|
team_ids = organization_team_ids(body[:team_id] || body[:team_ids])
|
|
349
|
+
ensure_team_member_capacity!(ctx, config, team_ids)
|
|
350
|
+
invitation_data = {
|
|
351
|
+
organizationId: organization["id"],
|
|
352
|
+
email: email,
|
|
353
|
+
role: role,
|
|
354
|
+
status: "pending",
|
|
355
|
+
expiresAt: Time.now + config[:invitation_expires_in].to_i,
|
|
356
|
+
inviterId: session[:user]["id"],
|
|
357
|
+
teamId: team_ids.any? ? team_ids.join(",") : nil,
|
|
358
|
+
createdAt: Time.now
|
|
359
|
+
}.merge(additional_input(body, :organization_id, :organization_slug, :email, :role, :team_id, :team_ids))
|
|
360
|
+
merge_hook_data!(invitation_data, run_org_hook(config, :before_create_invitation, {invitation: invitation_data, inviter: session[:user], organization: organization_wire(ctx, organization)}, ctx))
|
|
337
361
|
invitation = ctx.context.adapter.create(
|
|
338
362
|
model: "invitation",
|
|
339
|
-
data:
|
|
340
|
-
|
|
341
|
-
email: email,
|
|
342
|
-
role: role,
|
|
343
|
-
status: "pending",
|
|
344
|
-
expiresAt: Time.now + config[:invitation_expires_in].to_i,
|
|
345
|
-
inviterId: session[:user]["id"],
|
|
346
|
-
teamId: team_ids.any? ? team_ids.join(",") : nil,
|
|
347
|
-
createdAt: Time.now
|
|
348
|
-
}
|
|
363
|
+
data: invitation_data,
|
|
364
|
+
force_allow_id: true
|
|
349
365
|
)
|
|
350
366
|
sender = config[:send_invitation_email]
|
|
351
367
|
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
|
+
run_org_hook(config, :after_create_invitation, {invitation: invitation_wire(ctx, invitation), inviter: session[:user], organization: organization_wire(ctx, organization)}, ctx)
|
|
352
369
|
ctx.json(invitation_wire(ctx, invitation))
|
|
353
370
|
end
|
|
354
371
|
end
|
|
355
372
|
|
|
356
373
|
def organization_accept_invitation_endpoint(config)
|
|
357
|
-
Endpoint.new(path: "/organization/accept-invitation", method: "POST") do |ctx|
|
|
374
|
+
Endpoint.new(path: "/organization/accept-invitation", method: "POST", metadata: organization_openapi("acceptOrganizationInvitation", "Accept an organization invitation", response: organization_accept_invitation_schema)) do |ctx|
|
|
358
375
|
session = Routes.current_session(ctx)
|
|
359
376
|
body = normalize_hash(ctx.body)
|
|
360
377
|
invitation = invitation_by_id(ctx, body[:invitation_id] || body[:id])
|
|
@@ -365,6 +382,7 @@ module BetterAuth
|
|
|
365
382
|
if config[:require_email_verification_on_invitation] && !session[:user]["emailVerified"]
|
|
366
383
|
raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION"))
|
|
367
384
|
end
|
|
385
|
+
ensure_team_member_capacity!(ctx, config, organization_team_ids(invitation["teamId"]))
|
|
368
386
|
member = ctx.context.adapter.create(model: "member", data: {organizationId: invitation["organizationId"], userId: session[:user]["id"], role: invitation["role"], createdAt: Time.now})
|
|
369
387
|
organization_team_ids(invitation["teamId"]).each do |team_id|
|
|
370
388
|
ctx.context.adapter.create(model: "teamMember", data: {teamId: team_id, userId: session[:user]["id"], createdAt: Time.now})
|
|
@@ -377,7 +395,7 @@ module BetterAuth
|
|
|
377
395
|
end
|
|
378
396
|
|
|
379
397
|
def organization_reject_invitation_endpoint(_config)
|
|
380
|
-
Endpoint.new(path: "/organization/reject-invitation", method: "POST") do |ctx|
|
|
398
|
+
Endpoint.new(path: "/organization/reject-invitation", method: "POST", metadata: organization_openapi("rejectOrganizationInvitation", "Reject an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
|
|
381
399
|
session = Routes.current_session(ctx)
|
|
382
400
|
invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
|
|
383
401
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
|
|
@@ -388,7 +406,7 @@ module BetterAuth
|
|
|
388
406
|
end
|
|
389
407
|
|
|
390
408
|
def organization_cancel_invitation_endpoint(config)
|
|
391
|
-
Endpoint.new(path: "/organization/cancel-invitation", method: "POST") do |ctx|
|
|
409
|
+
Endpoint.new(path: "/organization/cancel-invitation", method: "POST", metadata: organization_openapi("cancelOrganizationInvitation", "Cancel an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
|
|
392
410
|
session = Routes.current_session(ctx)
|
|
393
411
|
invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
|
|
394
412
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
|
|
@@ -399,7 +417,7 @@ module BetterAuth
|
|
|
399
417
|
end
|
|
400
418
|
|
|
401
419
|
def organization_get_invitation_endpoint
|
|
402
|
-
Endpoint.new(path: "/organization/get-invitation", method: "GET") do |ctx|
|
|
420
|
+
Endpoint.new(path: "/organization/get-invitation", method: "GET", metadata: organization_openapi("getOrganizationInvitation", "Get an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
|
|
403
421
|
invitation = invitation_by_id(ctx, normalize_hash(ctx.query)[:id] || normalize_hash(ctx.query)[:invitation_id])
|
|
404
422
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
|
|
405
423
|
ctx.json(invitation_wire(ctx, invitation))
|
|
@@ -407,7 +425,7 @@ module BetterAuth
|
|
|
407
425
|
end
|
|
408
426
|
|
|
409
427
|
def organization_list_invitations_endpoint(config)
|
|
410
|
-
Endpoint.new(path: "/organization/list-invitations", method: "GET") do |ctx|
|
|
428
|
+
Endpoint.new(path: "/organization/list-invitations", method: "GET", metadata: organization_openapi("listOrganizationInvitations", "List organization invitations", response: organization_array_schema("Invitation"))) do |ctx|
|
|
411
429
|
session = Routes.current_session(ctx)
|
|
412
430
|
organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
|
|
413
431
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
|
|
@@ -418,7 +436,7 @@ module BetterAuth
|
|
|
418
436
|
end
|
|
419
437
|
|
|
420
438
|
def organization_list_user_invitations_endpoint
|
|
421
|
-
Endpoint.new(path: "/organization/list-user-invitations", method: "GET") do |ctx|
|
|
439
|
+
Endpoint.new(path: "/organization/list-user-invitations", method: "GET", metadata: organization_openapi("listUserInvitations", "List user invitations", response: organization_array_schema("Invitation"))) do |ctx|
|
|
422
440
|
session = Routes.current_session(ctx)
|
|
423
441
|
invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "email", value: session[:user]["email"].to_s.downcase}, {field: "status", value: "pending"}])
|
|
424
442
|
ctx.json(invitations.map { |entry| invitation_wire(ctx, entry) })
|
|
@@ -426,7 +444,7 @@ module BetterAuth
|
|
|
426
444
|
end
|
|
427
445
|
|
|
428
446
|
def organization_add_member_endpoint(config)
|
|
429
|
-
Endpoint.new(path: "/organization/add-member", method: "POST") do |ctx|
|
|
447
|
+
Endpoint.new(path: "/organization/add-member", method: "POST", metadata: organization_openapi("addOrganizationMember", "Add an organization member", response: organization_ref_schema("Member"))) do |ctx|
|
|
430
448
|
session = Routes.current_session(ctx)
|
|
431
449
|
body = normalize_hash(ctx.body)
|
|
432
450
|
organization_id = body[:organization_id]
|
|
@@ -447,7 +465,7 @@ module BetterAuth
|
|
|
447
465
|
end
|
|
448
466
|
|
|
449
467
|
def organization_remove_member_endpoint(config)
|
|
450
|
-
Endpoint.new(path: "/organization/remove-member", method: "POST") do |ctx|
|
|
468
|
+
Endpoint.new(path: "/organization/remove-member", method: "POST", metadata: organization_openapi("removeOrganizationMember", "Remove an organization member", response: OpenAPI.status_response_schema)) do |ctx|
|
|
451
469
|
session = Routes.current_session(ctx)
|
|
452
470
|
body = normalize_hash(ctx.body)
|
|
453
471
|
member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
|
|
@@ -464,7 +482,7 @@ module BetterAuth
|
|
|
464
482
|
end
|
|
465
483
|
|
|
466
484
|
def organization_update_member_role_endpoint(config)
|
|
467
|
-
Endpoint.new(path: "/organization/update-member-role", method: "POST") do |ctx|
|
|
485
|
+
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|
|
|
468
486
|
session = Routes.current_session(ctx)
|
|
469
487
|
body = normalize_hash(ctx.body)
|
|
470
488
|
member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
|
|
@@ -476,7 +494,7 @@ module BetterAuth
|
|
|
476
494
|
end
|
|
477
495
|
|
|
478
496
|
def organization_get_active_member_endpoint(_config)
|
|
479
|
-
Endpoint.new(path: "/organization/get-active-member", method: "GET") do |ctx|
|
|
497
|
+
Endpoint.new(path: "/organization/get-active-member", method: "GET", metadata: organization_openapi("getActiveOrganizationMember", "Get the active organization member", response: organization_ref_schema("Member"))) do |ctx|
|
|
480
498
|
session = Routes.current_session(ctx)
|
|
481
499
|
organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
|
|
482
500
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
|
|
@@ -486,17 +504,19 @@ module BetterAuth
|
|
|
486
504
|
end
|
|
487
505
|
|
|
488
506
|
def organization_get_active_member_role_endpoint(_config)
|
|
489
|
-
Endpoint.new(path: "/organization/get-active-member-role", method: "GET") do |ctx|
|
|
507
|
+
Endpoint.new(path: "/organization/get-active-member-role", method: "GET", metadata: organization_openapi("getActiveOrganizationMemberRole", "Get the active organization member role", response: organization_active_member_role_schema)) do |ctx|
|
|
490
508
|
session = Routes.current_session(ctx)
|
|
491
|
-
|
|
509
|
+
query = normalize_hash(ctx.query)
|
|
510
|
+
organization_id = query[:organization_id] || session[:session]["activeOrganizationId"]
|
|
492
511
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
|
|
493
|
-
|
|
494
|
-
ctx
|
|
512
|
+
require_member!(ctx, session[:user]["id"], organization_id)
|
|
513
|
+
member = require_member!(ctx, query[:user_id] || session[:user]["id"], organization_id)
|
|
514
|
+
ctx.json({role: member["role"], member: member_wire(ctx, member)})
|
|
495
515
|
end
|
|
496
516
|
end
|
|
497
517
|
|
|
498
518
|
def organization_leave_endpoint(config)
|
|
499
|
-
Endpoint.new(path: "/organization/leave", method: "POST") do |ctx|
|
|
519
|
+
Endpoint.new(path: "/organization/leave", method: "POST", metadata: organization_openapi("leaveOrganization", "Leave an organization", response: OpenAPI.status_response_schema)) do |ctx|
|
|
500
520
|
session = Routes.current_session(ctx)
|
|
501
521
|
organization_id = normalize_hash(ctx.body)[:organization_id]
|
|
502
522
|
member = require_member!(ctx, session[:user]["id"], organization_id)
|
|
@@ -508,10 +528,10 @@ module BetterAuth
|
|
|
508
528
|
end
|
|
509
529
|
|
|
510
530
|
def organization_list_members_endpoint(_config)
|
|
511
|
-
Endpoint.new(path: "/organization/list-members", method: "GET") do |ctx|
|
|
531
|
+
Endpoint.new(path: "/organization/list-members", method: "GET", metadata: organization_openapi("listOrganizationMembers", "List organization members", response: organization_members_response_schema)) do |ctx|
|
|
512
532
|
session = Routes.current_session(ctx)
|
|
513
533
|
query = normalize_hash(ctx.query)
|
|
514
|
-
organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id")
|
|
534
|
+
organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id") || session[:session]["activeOrganizationId"]
|
|
515
535
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
|
|
516
536
|
require_member!(ctx, session[:user]["id"], organization_id)
|
|
517
537
|
ctx.json(list_members_for(ctx, organization_id, query))
|
|
@@ -519,7 +539,7 @@ module BetterAuth
|
|
|
519
539
|
end
|
|
520
540
|
|
|
521
541
|
def organization_has_permission_endpoint(config)
|
|
522
|
-
Endpoint.new(path: "/organization/has-permission", method: "POST") do |ctx|
|
|
542
|
+
Endpoint.new(path: "/organization/has-permission", method: "POST", metadata: organization_openapi("hasOrganizationPermission", "Check if the member has organization permission", response: organization_permission_response_schema)) do |ctx|
|
|
523
543
|
session = Routes.current_session(ctx)
|
|
524
544
|
body = normalize_hash(ctx.body)
|
|
525
545
|
organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
|
|
@@ -531,7 +551,7 @@ module BetterAuth
|
|
|
531
551
|
end
|
|
532
552
|
|
|
533
553
|
def organization_create_team_endpoint(config)
|
|
534
|
-
Endpoint.new(path: "/organization/create-team", method: "POST") do |ctx|
|
|
554
|
+
Endpoint.new(path: "/organization/create-team", method: "POST", metadata: organization_openapi("createOrganizationTeam", "Create an organization team", response: organization_ref_schema("Team"))) do |ctx|
|
|
535
555
|
session = Routes.current_session(ctx)
|
|
536
556
|
body = normalize_hash(ctx.body)
|
|
537
557
|
organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
|
|
@@ -543,7 +563,7 @@ module BetterAuth
|
|
|
543
563
|
end
|
|
544
564
|
team_data = {organizationId: organization_id, name: body[:name].to_s, createdAt: Time.now}.merge(additional_input(body, :organization_id, :name))
|
|
545
565
|
merge_hook_data!(team_data, run_org_hook(config, :before_create_team, {team: team_data, user: session[:user], organization: organization_wire(ctx, organization)}, ctx))
|
|
546
|
-
team = ctx.context.adapter.create(model: "team", data: team_data)
|
|
566
|
+
team = ctx.context.adapter.create(model: "team", data: team_data, force_allow_id: true)
|
|
547
567
|
ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
|
|
548
568
|
run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
|
|
549
569
|
ctx.json(team_wire(ctx, team))
|
|
@@ -551,7 +571,7 @@ module BetterAuth
|
|
|
551
571
|
end
|
|
552
572
|
|
|
553
573
|
def organization_list_teams_endpoint(_config)
|
|
554
|
-
Endpoint.new(path: "/organization/list-teams", method: "GET") do |ctx|
|
|
574
|
+
Endpoint.new(path: "/organization/list-teams", method: "GET", metadata: organization_openapi("listOrganizationTeams", "List organization teams", response: organization_array_schema("Team"))) do |ctx|
|
|
555
575
|
session = Routes.current_session(ctx)
|
|
556
576
|
organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
|
|
557
577
|
require_member!(ctx, session[:user]["id"], organization_id)
|
|
@@ -561,7 +581,7 @@ module BetterAuth
|
|
|
561
581
|
end
|
|
562
582
|
|
|
563
583
|
def organization_update_team_endpoint(config)
|
|
564
|
-
Endpoint.new(path: "/organization/update-team", method: "POST") do |ctx|
|
|
584
|
+
Endpoint.new(path: "/organization/update-team", method: "POST", metadata: organization_openapi("updateOrganizationTeam", "Update an organization team", response: organization_ref_schema("Team"))) do |ctx|
|
|
565
585
|
session = Routes.current_session(ctx)
|
|
566
586
|
body = normalize_hash(ctx.body)
|
|
567
587
|
team = team_by_id(ctx, body[:team_id])
|
|
@@ -573,7 +593,7 @@ module BetterAuth
|
|
|
573
593
|
end
|
|
574
594
|
|
|
575
595
|
def organization_remove_team_endpoint(config)
|
|
576
|
-
Endpoint.new(path: "/organization/remove-team", method: "POST") do |ctx|
|
|
596
|
+
Endpoint.new(path: "/organization/remove-team", method: "POST", metadata: organization_openapi("removeOrganizationTeam", "Remove an organization team", response: OpenAPI.status_response_schema)) do |ctx|
|
|
577
597
|
session = Routes.current_session(ctx)
|
|
578
598
|
team = team_by_id(ctx, normalize_hash(ctx.body)[:team_id])
|
|
579
599
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
|
|
@@ -589,7 +609,7 @@ module BetterAuth
|
|
|
589
609
|
end
|
|
590
610
|
|
|
591
611
|
def organization_set_active_team_endpoint(_config)
|
|
592
|
-
Endpoint.new(path: "/organization/set-active-team", method: "POST") do |ctx|
|
|
612
|
+
Endpoint.new(path: "/organization/set-active-team", method: "POST", metadata: organization_openapi("setActiveOrganizationTeam", "Set the active organization team", response: organization_nullable_schema("Team"))) do |ctx|
|
|
593
613
|
session = Routes.current_session(ctx)
|
|
594
614
|
body = normalize_hash(ctx.body)
|
|
595
615
|
if body.key?(:team_id) && body[:team_id].nil?
|
|
@@ -607,7 +627,7 @@ module BetterAuth
|
|
|
607
627
|
end
|
|
608
628
|
|
|
609
629
|
def organization_list_user_teams_endpoint
|
|
610
|
-
Endpoint.new(path: "/organization/list-user-teams", method: "GET") do |ctx|
|
|
630
|
+
Endpoint.new(path: "/organization/list-user-teams", method: "GET", metadata: organization_openapi("listUserTeams", "List user teams", response: organization_array_schema("Team"))) do |ctx|
|
|
611
631
|
session = Routes.current_session(ctx)
|
|
612
632
|
memberships = ctx.context.adapter.find_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}])
|
|
613
633
|
ctx.json(memberships.filter_map { |entry| team_by_id(ctx, entry["teamId"]) }.map { |team| team_wire(ctx, team) })
|
|
@@ -615,7 +635,7 @@ module BetterAuth
|
|
|
615
635
|
end
|
|
616
636
|
|
|
617
637
|
def organization_list_team_members_endpoint(_config)
|
|
618
|
-
Endpoint.new(path: "/organization/list-team-members", method: "GET") do |ctx|
|
|
638
|
+
Endpoint.new(path: "/organization/list-team-members", method: "GET", metadata: organization_openapi("listTeamMembers", "List team members", response: organization_array_schema("TeamMember"))) do |ctx|
|
|
619
639
|
session = Routes.current_session(ctx)
|
|
620
640
|
team_id = normalize_hash(ctx.query)[:team_id] || session[:session]["activeTeamId"]
|
|
621
641
|
raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM")) unless team_id
|
|
@@ -627,7 +647,7 @@ module BetterAuth
|
|
|
627
647
|
end
|
|
628
648
|
|
|
629
649
|
def organization_add_team_member_endpoint(config)
|
|
630
|
-
Endpoint.new(path: "/organization/add-team-member", method: "POST") do |ctx|
|
|
650
|
+
Endpoint.new(path: "/organization/add-team-member", method: "POST", metadata: organization_openapi("addTeamMember", "Add a team member", response: organization_ref_schema("TeamMember"))) do |ctx|
|
|
631
651
|
session = Routes.current_session(ctx)
|
|
632
652
|
body = normalize_hash(ctx.body)
|
|
633
653
|
team = team_by_id(ctx, body[:team_id])
|
|
@@ -648,7 +668,7 @@ module BetterAuth
|
|
|
648
668
|
end
|
|
649
669
|
|
|
650
670
|
def organization_remove_team_member_endpoint(config)
|
|
651
|
-
Endpoint.new(path: "/organization/remove-team-member", method: "POST") do |ctx|
|
|
671
|
+
Endpoint.new(path: "/organization/remove-team-member", method: "POST", metadata: organization_openapi("removeTeamMember", "Remove a team member", response: OpenAPI.status_response_schema)) do |ctx|
|
|
652
672
|
session = Routes.current_session(ctx)
|
|
653
673
|
body = normalize_hash(ctx.body)
|
|
654
674
|
team = team_by_id(ctx, body[:team_id])
|
|
@@ -660,7 +680,7 @@ module BetterAuth
|
|
|
660
680
|
end
|
|
661
681
|
|
|
662
682
|
def organization_create_role_endpoint(config)
|
|
663
|
-
Endpoint.new(path: "/organization/create-role", method: "POST") do |ctx|
|
|
683
|
+
Endpoint.new(path: "/organization/create-role", method: "POST", metadata: organization_openapi("createOrganizationRole", "Create an organization role", response: organization_role_action_schema)) do |ctx|
|
|
664
684
|
session = Routes.current_session(ctx)
|
|
665
685
|
body = normalize_hash(ctx.body)
|
|
666
686
|
organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
|
|
@@ -679,7 +699,7 @@ module BetterAuth
|
|
|
679
699
|
end
|
|
680
700
|
|
|
681
701
|
def organization_list_roles_endpoint(config)
|
|
682
|
-
Endpoint.new(path: "/organization/list-roles", method: "GET") do |ctx|
|
|
702
|
+
Endpoint.new(path: "/organization/list-roles", method: "GET", metadata: organization_openapi("listOrganizationRoles", "List organization roles", response: {type: "array", items: organization_role_schema})) do |ctx|
|
|
683
703
|
session = Routes.current_session(ctx)
|
|
684
704
|
organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
|
|
685
705
|
require_org_permission!(ctx, config, session, organization_id, {ac: ["read"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE"))
|
|
@@ -690,7 +710,7 @@ module BetterAuth
|
|
|
690
710
|
end
|
|
691
711
|
|
|
692
712
|
def organization_get_role_endpoint(config)
|
|
693
|
-
Endpoint.new(path: "/organization/get-role", method: "GET") do |ctx|
|
|
713
|
+
Endpoint.new(path: "/organization/get-role", method: "GET", metadata: organization_openapi("getOrganizationRole", "Get an organization role", response: organization_role_schema)) do |ctx|
|
|
694
714
|
session = Routes.current_session(ctx)
|
|
695
715
|
query = normalize_hash(ctx.query)
|
|
696
716
|
organization_id = query[:organization_id] || session[:session]["activeOrganizationId"]
|
|
@@ -702,7 +722,7 @@ module BetterAuth
|
|
|
702
722
|
end
|
|
703
723
|
|
|
704
724
|
def organization_update_role_endpoint(config)
|
|
705
|
-
Endpoint.new(path: "/organization/update-role", method: "POST") do |ctx|
|
|
725
|
+
Endpoint.new(path: "/organization/update-role", method: "POST", metadata: organization_openapi("updateOrganizationRole", "Update an organization role", response: organization_role_action_schema)) do |ctx|
|
|
706
726
|
session = Routes.current_session(ctx)
|
|
707
727
|
body = normalize_hash(ctx.body)
|
|
708
728
|
organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
|
|
@@ -727,7 +747,7 @@ module BetterAuth
|
|
|
727
747
|
end
|
|
728
748
|
|
|
729
749
|
def organization_delete_role_endpoint(config)
|
|
730
|
-
Endpoint.new(path: "/organization/delete-role", method: "POST") do |ctx|
|
|
750
|
+
Endpoint.new(path: "/organization/delete-role", method: "POST", metadata: organization_openapi("deleteOrganizationRole", "Delete an organization role", response: OpenAPI.success_response_schema)) do |ctx|
|
|
731
751
|
session = Routes.current_session(ctx)
|
|
732
752
|
body = normalize_hash(ctx.body)
|
|
733
753
|
organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
|
|
@@ -747,6 +767,104 @@ module BetterAuth
|
|
|
747
767
|
end
|
|
748
768
|
end
|
|
749
769
|
|
|
770
|
+
def organization_openapi(operation_id, description, response:, response_description: "Success", request: nil, required: [], parameters: nil)
|
|
771
|
+
openapi = {
|
|
772
|
+
operationId: operation_id,
|
|
773
|
+
description: description,
|
|
774
|
+
responses: {
|
|
775
|
+
"200" => OpenAPI.json_response(response_description, response)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
openapi[:requestBody] = OpenAPI.json_request_body(OpenAPI.object_schema(request, required: required)) if request
|
|
779
|
+
openapi[:parameters] = parameters if parameters
|
|
780
|
+
|
|
781
|
+
{openapi: openapi}
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def organization_ref_schema(name)
|
|
785
|
+
{
|
|
786
|
+
type: "object",
|
|
787
|
+
"$ref": "#/components/schemas/#{name}"
|
|
788
|
+
}
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def organization_nullable_schema(name)
|
|
792
|
+
{
|
|
793
|
+
type: ["object", "null"],
|
|
794
|
+
"$ref": "#/components/schemas/#{name}"
|
|
795
|
+
}
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def organization_array_schema(name)
|
|
799
|
+
{
|
|
800
|
+
type: "array",
|
|
801
|
+
items: organization_ref_schema(name)
|
|
802
|
+
}
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
def organization_accept_invitation_schema
|
|
806
|
+
OpenAPI.object_schema(
|
|
807
|
+
{
|
|
808
|
+
invitation: organization_ref_schema("Invitation"),
|
|
809
|
+
member: organization_ref_schema("Member")
|
|
810
|
+
},
|
|
811
|
+
required: ["invitation", "member"]
|
|
812
|
+
)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def organization_active_member_role_schema
|
|
816
|
+
OpenAPI.object_schema(
|
|
817
|
+
{
|
|
818
|
+
role: {type: "string"},
|
|
819
|
+
member: organization_ref_schema("Member")
|
|
820
|
+
},
|
|
821
|
+
required: ["role", "member"]
|
|
822
|
+
)
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def organization_members_response_schema
|
|
826
|
+
OpenAPI.object_schema(
|
|
827
|
+
{
|
|
828
|
+
members: organization_array_schema("Member"),
|
|
829
|
+
total: {type: "number"}
|
|
830
|
+
},
|
|
831
|
+
required: ["members", "total"]
|
|
832
|
+
)
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def organization_permission_response_schema
|
|
836
|
+
OpenAPI.object_schema(
|
|
837
|
+
{
|
|
838
|
+
error: {type: ["string", "null"]},
|
|
839
|
+
success: {type: "boolean"}
|
|
840
|
+
},
|
|
841
|
+
required: ["success"]
|
|
842
|
+
)
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def organization_role_schema
|
|
846
|
+
OpenAPI.object_schema(
|
|
847
|
+
{
|
|
848
|
+
id: {type: "string"},
|
|
849
|
+
organizationId: {type: "string"},
|
|
850
|
+
role: {type: "string"},
|
|
851
|
+
permission: {type: "object"}
|
|
852
|
+
},
|
|
853
|
+
required: ["role", "permission"]
|
|
854
|
+
)
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def organization_role_action_schema
|
|
858
|
+
OpenAPI.object_schema(
|
|
859
|
+
{
|
|
860
|
+
success: {type: "boolean"},
|
|
861
|
+
roleData: organization_role_schema,
|
|
862
|
+
statements: {type: "object"}
|
|
863
|
+
},
|
|
864
|
+
required: ["success", "roleData"]
|
|
865
|
+
)
|
|
866
|
+
end
|
|
867
|
+
|
|
750
868
|
def parse_roles(roles)
|
|
751
869
|
Array(roles).join(",")
|
|
752
870
|
end
|
|
@@ -864,7 +982,7 @@ module BetterAuth
|
|
|
864
982
|
where: where,
|
|
865
983
|
limit: query[:limit],
|
|
866
984
|
offset: query[:offset],
|
|
867
|
-
sort_by: query[:sort_by] ? {field: query[:sort_by], direction: query[:sort_order] || "asc"} : nil
|
|
985
|
+
sort_by: query[:sort_by] ? {field: query[:sort_by], direction: query[:sort_direction] || query[:sort_order] || "asc"} : nil
|
|
868
986
|
)
|
|
869
987
|
{
|
|
870
988
|
members: members.map { |entry| member_wire(ctx, entry) },
|
|
@@ -872,6 +990,18 @@ module BetterAuth
|
|
|
872
990
|
}
|
|
873
991
|
end
|
|
874
992
|
|
|
993
|
+
def ensure_team_member_capacity!(ctx, config, team_ids)
|
|
994
|
+
max_members = config.dig(:teams, :maximum_members_per_team)
|
|
995
|
+
return unless max_members && team_ids.any?
|
|
996
|
+
|
|
997
|
+
team_ids.each do |team_id|
|
|
998
|
+
count = ctx.context.adapter.count(model: "teamMember", where: [{field: "teamId", value: team_id}])
|
|
999
|
+
if count >= max_members.to_i
|
|
1000
|
+
raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_MEMBER_LIMIT_REACHED"))
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
|
|
875
1005
|
def member_wire(ctx, member)
|
|
876
1006
|
data = Schema.parse_output(ctx.context.options, "member", member)
|
|
877
1007
|
user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
|
|
@@ -915,7 +1045,7 @@ module BetterAuth
|
|
|
915
1045
|
team = if custom.respond_to?(:call)
|
|
916
1046
|
custom.call(organization_wire(ctx, organization), ctx)
|
|
917
1047
|
else
|
|
918
|
-
ctx.context.adapter.create(model: "team", data: team_data)
|
|
1048
|
+
ctx.context.adapter.create(model: "team", data: team_data, force_allow_id: true)
|
|
919
1049
|
end
|
|
920
1050
|
ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
|
|
921
1051
|
run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
|