better_auth 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +110 -18
  4. data/lib/better_auth/adapters/base.rb +49 -0
  5. data/lib/better_auth/adapters/internal_adapter.rb +589 -0
  6. data/lib/better_auth/adapters/memory.rb +235 -0
  7. data/lib/better_auth/adapters/mongodb.rb +9 -0
  8. data/lib/better_auth/adapters/mssql.rb +42 -0
  9. data/lib/better_auth/adapters/mysql.rb +33 -0
  10. data/lib/better_auth/adapters/postgres.rb +17 -0
  11. data/lib/better_auth/adapters/sql.rb +441 -0
  12. data/lib/better_auth/adapters/sqlite.rb +20 -0
  13. data/lib/better_auth/api.rb +226 -0
  14. data/lib/better_auth/api_error.rb +53 -0
  15. data/lib/better_auth/auth.rb +42 -0
  16. data/lib/better_auth/configuration.rb +399 -0
  17. data/lib/better_auth/context.rb +211 -0
  18. data/lib/better_auth/cookies.rb +278 -0
  19. data/lib/better_auth/core.rb +37 -1
  20. data/lib/better_auth/crypto/jwe.rb +76 -0
  21. data/lib/better_auth/crypto.rb +191 -0
  22. data/lib/better_auth/database_hooks.rb +114 -0
  23. data/lib/better_auth/endpoint.rb +326 -0
  24. data/lib/better_auth/error.rb +52 -0
  25. data/lib/better_auth/middleware/origin_check.rb +128 -0
  26. data/lib/better_auth/password.rb +120 -0
  27. data/lib/better_auth/plugin.rb +142 -0
  28. data/lib/better_auth/plugin_context.rb +16 -0
  29. data/lib/better_auth/plugin_registry.rb +67 -0
  30. data/lib/better_auth/plugins/access.rb +87 -0
  31. data/lib/better_auth/plugins/additional_fields.rb +29 -0
  32. data/lib/better_auth/plugins/admin/schema.rb +28 -0
  33. data/lib/better_auth/plugins/admin.rb +518 -0
  34. data/lib/better_auth/plugins/anonymous.rb +198 -0
  35. data/lib/better_auth/plugins/api_key.rb +16 -0
  36. data/lib/better_auth/plugins/bearer.rb +128 -0
  37. data/lib/better_auth/plugins/captcha.rb +159 -0
  38. data/lib/better_auth/plugins/custom_session.rb +84 -0
  39. data/lib/better_auth/plugins/device_authorization.rb +302 -0
  40. data/lib/better_auth/plugins/email_otp.rb +536 -0
  41. data/lib/better_auth/plugins/expo.rb +88 -0
  42. data/lib/better_auth/plugins/generic_oauth.rb +780 -0
  43. data/lib/better_auth/plugins/have_i_been_pwned.rb +94 -0
  44. data/lib/better_auth/plugins/jwt.rb +482 -0
  45. data/lib/better_auth/plugins/last_login_method.rb +92 -0
  46. data/lib/better_auth/plugins/magic_link.rb +181 -0
  47. data/lib/better_auth/plugins/mcp.rb +342 -0
  48. data/lib/better_auth/plugins/multi_session.rb +173 -0
  49. data/lib/better_auth/plugins/oauth_protocol.rb +694 -0
  50. data/lib/better_auth/plugins/oauth_provider.rb +16 -0
  51. data/lib/better_auth/plugins/oauth_proxy.rb +257 -0
  52. data/lib/better_auth/plugins/oidc_provider.rb +597 -0
  53. data/lib/better_auth/plugins/one_tap.rb +154 -0
  54. data/lib/better_auth/plugins/one_time_token.rb +106 -0
  55. data/lib/better_auth/plugins/open_api.rb +489 -0
  56. data/lib/better_auth/plugins/organization/schema.rb +106 -0
  57. data/lib/better_auth/plugins/organization.rb +995 -0
  58. data/lib/better_auth/plugins/passkey.rb +16 -0
  59. data/lib/better_auth/plugins/phone_number.rb +321 -0
  60. data/lib/better_auth/plugins/scim.rb +16 -0
  61. data/lib/better_auth/plugins/siwe.rb +242 -0
  62. data/lib/better_auth/plugins/sso.rb +16 -0
  63. data/lib/better_auth/plugins/stripe.rb +16 -0
  64. data/lib/better_auth/plugins/two_factor.rb +514 -0
  65. data/lib/better_auth/plugins/username.rb +278 -0
  66. data/lib/better_auth/plugins.rb +46 -0
  67. data/lib/better_auth/rate_limiter.rb +232 -0
  68. data/lib/better_auth/request_ip.rb +70 -0
  69. data/lib/better_auth/router.rb +378 -0
  70. data/lib/better_auth/routes/account.rb +211 -0
  71. data/lib/better_auth/routes/email_verification.rb +111 -0
  72. data/lib/better_auth/routes/error.rb +102 -0
  73. data/lib/better_auth/routes/ok.rb +15 -0
  74. data/lib/better_auth/routes/password.rb +183 -0
  75. data/lib/better_auth/routes/session.rb +160 -0
  76. data/lib/better_auth/routes/sign_in.rb +90 -0
  77. data/lib/better_auth/routes/sign_out.rb +15 -0
  78. data/lib/better_auth/routes/sign_up.rb +196 -0
  79. data/lib/better_auth/routes/social.rb +367 -0
  80. data/lib/better_auth/routes/user.rb +205 -0
  81. data/lib/better_auth/schema/sql.rb +202 -0
  82. data/lib/better_auth/schema.rb +291 -0
  83. data/lib/better_auth/session.rb +122 -0
  84. data/lib/better_auth/session_store.rb +91 -0
  85. data/lib/better_auth/social_providers/apple.rb +91 -0
  86. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  87. data/lib/better_auth/social_providers/base.rb +325 -0
  88. data/lib/better_auth/social_providers/cognito.rb +32 -0
  89. data/lib/better_auth/social_providers/discord.rb +81 -0
  90. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  91. data/lib/better_auth/social_providers/facebook.rb +35 -0
  92. data/lib/better_auth/social_providers/figma.rb +31 -0
  93. data/lib/better_auth/social_providers/github.rb +74 -0
  94. data/lib/better_auth/social_providers/gitlab.rb +67 -0
  95. data/lib/better_auth/social_providers/google.rb +90 -0
  96. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  97. data/lib/better_auth/social_providers/kakao.rb +32 -0
  98. data/lib/better_auth/social_providers/kick.rb +32 -0
  99. data/lib/better_auth/social_providers/line.rb +33 -0
  100. data/lib/better_auth/social_providers/linear.rb +44 -0
  101. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  102. data/lib/better_auth/social_providers/microsoft_entra_id.rb +137 -0
  103. data/lib/better_auth/social_providers/naver.rb +31 -0
  104. data/lib/better_auth/social_providers/notion.rb +33 -0
  105. data/lib/better_auth/social_providers/paybin.rb +31 -0
  106. data/lib/better_auth/social_providers/paypal.rb +36 -0
  107. data/lib/better_auth/social_providers/polar.rb +31 -0
  108. data/lib/better_auth/social_providers/railway.rb +49 -0
  109. data/lib/better_auth/social_providers/reddit.rb +32 -0
  110. data/lib/better_auth/social_providers/roblox.rb +31 -0
  111. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  112. data/lib/better_auth/social_providers/slack.rb +30 -0
  113. data/lib/better_auth/social_providers/spotify.rb +31 -0
  114. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  115. data/lib/better_auth/social_providers/twitch.rb +39 -0
  116. data/lib/better_auth/social_providers/twitter.rb +32 -0
  117. data/lib/better_auth/social_providers/vercel.rb +47 -0
  118. data/lib/better_auth/social_providers/vk.rb +34 -0
  119. data/lib/better_auth/social_providers/wechat.rb +104 -0
  120. data/lib/better_auth/social_providers/zoom.rb +31 -0
  121. data/lib/better_auth/social_providers.rb +38 -0
  122. data/lib/better_auth/version.rb +1 -1
  123. data/lib/better_auth.rb +86 -2
  124. metadata +233 -21
  125. data/.ruby-version +0 -1
  126. data/.standard.yml +0 -12
  127. data/.vscode/settings.json +0 -22
  128. data/AGENTS.md +0 -50
  129. data/CLAUDE.md +0 -1
  130. data/CODE_OF_CONDUCT.md +0 -173
  131. data/CONTRIBUTING.md +0 -187
  132. data/Gemfile +0 -12
  133. data/Makefile +0 -207
  134. data/Rakefile +0 -25
  135. data/SECURITY.md +0 -28
  136. data/docker-compose.yml +0 -63
@@ -0,0 +1,995 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module BetterAuth
6
+ module Plugins
7
+ ORGANIZATION_ERROR_CODES = {
8
+ "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION" => "You are not allowed to create a new organization",
9
+ "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS" => "You have reached the maximum number of organizations",
10
+ "ORGANIZATION_ALREADY_EXISTS" => "Organization already exists",
11
+ "ORGANIZATION_SLUG_ALREADY_TAKEN" => "Organization slug already taken",
12
+ "ORGANIZATION_NOT_FOUND" => "Organization not found",
13
+ "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION" => "User is not a member of the organization",
14
+ "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION" => "You are not allowed to update this organization",
15
+ "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION" => "You are not allowed to delete this organization",
16
+ "NO_ACTIVE_ORGANIZATION" => "No active organization",
17
+ "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION" => "User is already a member of this organization",
18
+ "MEMBER_NOT_FOUND" => "Member not found",
19
+ "ROLE_NOT_FOUND" => "Role not found",
20
+ "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM" => "You are not allowed to create a new team",
21
+ "TEAM_ALREADY_EXISTS" => "Team already exists",
22
+ "TEAM_NOT_FOUND" => "Team not found",
23
+ "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER" => "You cannot leave the organization as the only owner",
24
+ "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER" => "You cannot leave the organization without an owner",
25
+ "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER" => "You are not allowed to delete this member",
26
+ "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION" => "You are not allowed to invite users to this organization",
27
+ "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION" => "User is already invited to this organization",
28
+ "INVITATION_NOT_FOUND" => "Invitation not found",
29
+ "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION" => "You are not the recipient of the invitation",
30
+ "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION" => "Email verification required before accepting or rejecting invitation",
31
+ "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION" => "You are not allowed to cancel this invitation",
32
+ "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION" => "Inviter is no longer a member of the organization",
33
+ "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE" => "You are not allowed to invite a user with this role",
34
+ "FAILED_TO_RETRIEVE_INVITATION" => "Failed to retrieve invitation",
35
+ "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS" => "You have reached the maximum number of teams",
36
+ "UNABLE_TO_REMOVE_LAST_TEAM" => "Unable to remove last team",
37
+ "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER" => "You are not allowed to update this member",
38
+ "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED" => "Organization membership limit reached",
39
+ "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION" => "You are not allowed to create teams in this organization",
40
+ "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION" => "You are not allowed to delete teams in this organization",
41
+ "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM" => "You are not allowed to update this team",
42
+ "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM" => "You are not allowed to delete this team",
43
+ "INVITATION_LIMIT_REACHED" => "Invitation limit reached",
44
+ "TEAM_MEMBER_LIMIT_REACHED" => "Team member limit reached",
45
+ "USER_IS_NOT_A_MEMBER_OF_THE_TEAM" => "User is not a member of the team",
46
+ "YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM" => "You are not allowed to list the members of this team",
47
+ "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM" => "You do not have an active team",
48
+ "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER" => "You are not allowed to create a new member",
49
+ "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER" => "You are not allowed to remove a team member",
50
+ "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION" => "You are not allowed to access this organization as an owner",
51
+ "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION" => "You are not a member of this organization",
52
+ "MISSING_AC_INSTANCE" => "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information",
53
+ "YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE" => "You must be in an organization to create a role",
54
+ "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE" => "You are not allowed to create a role",
55
+ "YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE" => "You are not allowed to update a role",
56
+ "YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE" => "You are not allowed to delete a role",
57
+ "YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE" => "You are not allowed to read a role",
58
+ "YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE" => "You are not allowed to list a role",
59
+ "YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE" => "You are not allowed to get a role",
60
+ "TOO_MANY_ROLES" => "This organization has too many roles",
61
+ "INVALID_RESOURCE" => "The provided permission includes an invalid resource",
62
+ "ROLE_NAME_IS_ALREADY_TAKEN" => "That role name is already taken",
63
+ "CANNOT_DELETE_A_PRE_DEFINED_ROLE" => "Cannot delete a pre-defined role",
64
+ "ROLE_IS_ASSIGNED_TO_MEMBERS" => "Cannot delete a role that is assigned to members. Please reassign the members to a different role first"
65
+ }.freeze
66
+
67
+ ORGANIZATION_DEFAULT_STATEMENTS = {
68
+ organization: ["update", "delete"],
69
+ member: ["create", "update", "delete"],
70
+ invitation: ["create", "cancel"],
71
+ team: ["create", "update", "delete"],
72
+ ac: ["create", "read", "update", "delete"]
73
+ }.freeze
74
+
75
+ module_function
76
+
77
+ def organization(options = {})
78
+ config = organization_config(options)
79
+ endpoints = {
80
+ create_organization: organization_create_endpoint(config),
81
+ update_organization: organization_update_endpoint(config),
82
+ delete_organization: organization_delete_endpoint(config),
83
+ check_organization_slug: organization_check_slug_endpoint,
84
+ set_active_organization: organization_set_active_endpoint,
85
+ get_full_organization: organization_get_full_endpoint(config),
86
+ list_organizations: organization_list_endpoint,
87
+ create_invitation: organization_invite_endpoint(config),
88
+ cancel_invitation: organization_cancel_invitation_endpoint(config),
89
+ accept_invitation: organization_accept_invitation_endpoint(config),
90
+ reject_invitation: organization_reject_invitation_endpoint(config),
91
+ get_invitation: organization_get_invitation_endpoint,
92
+ list_invitations: organization_list_invitations_endpoint(config),
93
+ list_user_invitations: organization_list_user_invitations_endpoint,
94
+ add_member: organization_add_member_endpoint(config),
95
+ remove_member: organization_remove_member_endpoint(config),
96
+ update_member_role: organization_update_member_role_endpoint(config),
97
+ get_active_member: organization_get_active_member_endpoint(config),
98
+ leave_organization: organization_leave_endpoint(config),
99
+ list_members: organization_list_members_endpoint(config),
100
+ get_active_member_role: organization_get_active_member_role_endpoint(config),
101
+ has_permission: organization_has_permission_endpoint(config)
102
+ }
103
+
104
+ if org_truthy?(config.dig(:teams, :enabled))
105
+ endpoints.merge!(
106
+ create_team: organization_create_team_endpoint(config),
107
+ remove_team: organization_remove_team_endpoint(config),
108
+ update_team: organization_update_team_endpoint(config),
109
+ list_organization_teams: organization_list_teams_endpoint(config),
110
+ set_active_team: organization_set_active_team_endpoint(config),
111
+ list_user_teams: organization_list_user_teams_endpoint,
112
+ list_team_members: organization_list_team_members_endpoint(config),
113
+ add_team_member: organization_add_team_member_endpoint(config),
114
+ remove_team_member: organization_remove_team_member_endpoint(config)
115
+ )
116
+ end
117
+
118
+ if org_truthy?(config.dig(:dynamic_access_control, :enabled))
119
+ endpoints.merge!(
120
+ create_org_role: organization_create_role_endpoint(config),
121
+ delete_org_role: organization_delete_role_endpoint(config),
122
+ list_org_roles: organization_list_roles_endpoint(config),
123
+ get_org_role: organization_get_role_endpoint(config),
124
+ update_org_role: organization_update_role_endpoint(config)
125
+ )
126
+ end
127
+
128
+ Plugin.new(
129
+ id: "organization",
130
+ schema: OrganizationSchema.build(config),
131
+ endpoints: endpoints,
132
+ error_codes: ORGANIZATION_ERROR_CODES,
133
+ options: config
134
+ )
135
+ end
136
+
137
+ def organization_config(options)
138
+ config = normalize_hash(options)
139
+ config[:allow_user_to_create_organization] = true unless config.key?(:allow_user_to_create_organization)
140
+ config[:creator_role] ||= "owner"
141
+ config[:membership_limit] ||= 100
142
+ config[:invitation_expires_in] ||= 60 * 60 * 48
143
+ config[:invitation_limit] ||= 100
144
+ config[:ac] ||= create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)
145
+ config[:roles] ||= organization_default_roles(config)
146
+ config
147
+ end
148
+
149
+ def organization_default_roles(config = {})
150
+ ac = config[:ac] || create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)
151
+ {
152
+ "admin" => ac.new_role(organization: ["update"], invitation: ["create", "cancel"], member: ["create", "update", "delete"], team: ["create", "update", "delete"], ac: ["create", "read", "update", "delete"]),
153
+ "owner" => ac.new_role(organization: ["update", "delete"], member: ["create", "update", "delete"], invitation: ["create", "cancel"], team: ["create", "update", "delete"], ac: ["create", "read", "update", "delete"]),
154
+ "member" => ac.new_role(organization: [], member: [], invitation: [], team: [], ac: ["read"])
155
+ }
156
+ end
157
+
158
+ def organization_create_endpoint(config)
159
+ Endpoint.new(path: "/organization/create", method: "POST") do |ctx|
160
+ body = normalize_hash(ctx.body)
161
+ session = Routes.current_session(ctx, allow_nil: true)
162
+ user = session ? session[:user] : ctx.context.internal_adapter.find_user_by_id(body[:user_id])
163
+ raise APIError.new("UNAUTHORIZED") unless user
164
+ name = body[:name].to_s
165
+ slug = body[:slug].to_s
166
+ raise APIError.new("BAD_REQUEST", message: "name is required") if name.empty?
167
+ raise APIError.new("BAD_REQUEST", message: "slug is required") if slug.empty?
168
+
169
+ allowed = config[:allow_user_to_create_organization]
170
+ allowed = allowed.call(user) if allowed.respond_to?(:call)
171
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION")) unless allowed
172
+
173
+ if config[:organization_limit]
174
+ limit_reached = config[:organization_limit].respond_to?(:call) ? config[:organization_limit].call(user) : organization_created_count(ctx, user["id"]) >= config[:organization_limit].to_i
175
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS")) if limit_reached
176
+ end
177
+
178
+ if organization_by_slug(ctx, slug)
179
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_ALREADY_EXISTS"))
180
+ end
181
+
182
+ data = {
183
+ name: name,
184
+ slug: slug,
185
+ logo: body[:logo],
186
+ metadata: serialize_metadata(body[:metadata]),
187
+ createdAt: Time.now
188
+ }.merge(additional_input(body, :name, :slug, :logo, :metadata, :keep_current_active_organization, :user_id))
189
+ merge_hook_data!(data, run_org_hook(config, :before_create_organization, {organization: data, user: user}, ctx))
190
+ organization = ctx.context.adapter.create(model: "organization", data: data, force_allow_id: true)
191
+ member_data = {organizationId: organization["id"], userId: user["id"], role: config[:creator_role], createdAt: Time.now}
192
+ merge_hook_data!(member_data, run_org_hook(config, :before_add_member, {member: member_data, user: user, organization: organization_wire(ctx, organization)}, ctx))
193
+ member = ctx.context.adapter.create(model: "member", data: member_data)
194
+ run_org_hook(config, :after_add_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
195
+ default_team = create_default_team(ctx, config, organization, {user: user}) if org_truthy?(config.dig(:teams, :enabled)) && config.dig(:teams, :default_team, :enabled) != false
196
+ run_org_hook(config, :after_create_organization, {organization: organization_wire(ctx, organization), member: member, user: user}, ctx)
197
+ if session && !org_truthy?(body[:keep_current_active_organization])
198
+ update = {activeOrganizationId: organization["id"], activeTeamId: default_team && default_team["id"]}
199
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], update)
200
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge(update.transform_keys(&:to_s)), user: user})
201
+ end
202
+ ctx.json(organization_wire(ctx, organization).merge(members: [member_wire(ctx, member)]))
203
+ end
204
+ end
205
+
206
+ def organization_check_slug_endpoint
207
+ Endpoint.new(path: "/organization/check-slug", method: "POST") do |ctx|
208
+ slug = normalize_hash(ctx.body)[:slug].to_s
209
+ if slug.empty? || organization_by_slug(ctx, slug)
210
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_SLUG_ALREADY_TAKEN"))
211
+ end
212
+ ctx.json({status: true})
213
+ end
214
+ end
215
+
216
+ def organization_list_endpoint
217
+ Endpoint.new(path: "/organization/list", method: "GET") do |ctx|
218
+ session = Routes.current_session(ctx)
219
+ members = ctx.context.adapter.find_many(model: "member", where: [{field: "userId", value: session[:user]["id"]}])
220
+ organizations = members.filter_map { |member| organization_by_id(ctx, member["organizationId"]) }
221
+ ctx.json(organizations.map { |entry| organization_wire(ctx, entry) })
222
+ end
223
+ end
224
+
225
+ def organization_update_endpoint(config)
226
+ Endpoint.new(path: "/organization/update", method: "POST") do |ctx|
227
+ session = Routes.current_session(ctx)
228
+ body = normalize_hash(ctx.body)
229
+ id = body[:organization_id] || body[:organizationId]
230
+ data = normalize_hash(body[:data] || body)
231
+ organization = organization_by_id(ctx, id) || organization_by_slug(ctx, body[:organization_slug])
232
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
233
+ require_org_permission!(ctx, config, session, organization["id"], {organization: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION"))
234
+ if data[:slug] && data[:slug].to_s.empty?
235
+ raise APIError.new("BAD_REQUEST", message: "slug is required")
236
+ end
237
+ if data[:name] && data[:name].to_s.empty?
238
+ raise APIError.new("BAD_REQUEST", message: "name is required")
239
+ end
240
+ existing = data[:slug] ? organization_by_slug(ctx, data[:slug]) : nil
241
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_SLUG_ALREADY_TAKEN")) if existing && existing["id"] != organization["id"]
242
+ update = additional_input(data, :organization_id, :organizationId, :organization_slug, :data)
243
+ update[:metadata] = serialize_metadata(update[:metadata]) if update.key?(:metadata)
244
+ updated = ctx.context.adapter.update(model: "organization", where: [{field: "id", value: organization["id"]}], update: update)
245
+ run_org_hook(config, :after_update_organization, {organization: organization_wire(ctx, updated), user: session[:user]}, ctx)
246
+ ctx.json(organization_wire(ctx, updated))
247
+ end
248
+ end
249
+
250
+ def organization_delete_endpoint(config)
251
+ Endpoint.new(path: "/organization/delete", method: "POST") do |ctx|
252
+ session = Routes.current_session(ctx)
253
+ body = normalize_hash(ctx.body)
254
+ organization = organization_by_id(ctx, body[:organization_id]) || organization_by_slug(ctx, body[:organization_slug])
255
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
256
+ require_org_permission!(ctx, config, session, organization["id"], {organization: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION"))
257
+ run_org_hook(config, :before_delete_organization, {organization: organization_wire(ctx, organization), user: session[:user]}, ctx)
258
+ if org_truthy?(config.dig(:teams, :enabled))
259
+ team_ids = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: organization["id"]}]).map { |team| team["id"] }
260
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team_ids, operator: "in"}]) if team_ids.any?
261
+ ctx.context.adapter.delete_many(model: "team", where: [{field: "organizationId", value: organization["id"]}])
262
+ end
263
+ ctx.context.adapter.delete_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}])
264
+ ctx.context.adapter.delete_many(model: "member", where: [{field: "organizationId", value: organization["id"]}])
265
+ ctx.context.adapter.delete_many(model: "organizationRole", where: [{field: "organizationId", value: organization["id"]}]) if org_truthy?(config.dig(:dynamic_access_control, :enabled))
266
+ ctx.context.adapter.delete(model: "organization", where: [{field: "id", value: organization["id"]}])
267
+ ctx.json({status: true})
268
+ end
269
+ end
270
+
271
+ def organization_set_active_endpoint
272
+ Endpoint.new(path: "/organization/set-active", method: "POST") do |ctx|
273
+ session = Routes.current_session(ctx, sensitive: true)
274
+ body = normalize_hash(ctx.body)
275
+ organization = organization_by_id(ctx, body[:organization_id]) || organization_by_slug(ctx, body[:organization_slug])
276
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
277
+ require_member!(ctx, session[:user]["id"], organization["id"])
278
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeOrganizationId: organization["id"], activeTeamId: nil})
279
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeOrganizationId" => organization["id"], "activeTeamId" => nil), user: session[:user]})
280
+ ctx.json(organization_wire(ctx, organization))
281
+ end
282
+ end
283
+
284
+ def organization_get_full_endpoint(config)
285
+ Endpoint.new(path: "/organization/get-full-organization", method: "GET") do |ctx|
286
+ session = Routes.current_session(ctx)
287
+ query = normalize_hash(ctx.query)
288
+ organization = organization_by_slug(ctx, query[:organization_slug]) || organization_by_id(ctx, query[:organization_id] || session[:session]["activeOrganizationId"])
289
+ next ctx.json(nil) unless organization
290
+
291
+ require_member!(ctx, session[:user]["id"], organization["id"])
292
+ members = list_members_for(ctx, organization["id"])
293
+ invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}])
294
+ result = organization_wire(ctx, organization).merge(
295
+ members: members.fetch(:members),
296
+ invitations: invitations.map { |entry| invitation_wire(ctx, entry) }
297
+ )
298
+ if org_truthy?(config.dig(:teams, :enabled))
299
+ teams = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: organization["id"]}])
300
+ result[:teams] = teams.map { |team| team_wire(ctx, team) }
301
+ end
302
+ ctx.json(result)
303
+ end
304
+ end
305
+
306
+ def organization_invite_endpoint(config)
307
+ Endpoint.new(path: "/organization/invite-member", method: "POST") do |ctx|
308
+ session = Routes.current_session(ctx)
309
+ body = normalize_hash(ctx.body)
310
+ organization = organization_by_id(ctx, body[:organization_id])
311
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless organization
312
+ 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
+ email = body[:email].to_s.downcase
314
+ raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("INVALID_EMAIL")) unless Routes::EMAIL_PATTERN.match?(email)
315
+ role = parse_roles(body[:role] || "member")
316
+ role.split(",").each do |entry|
317
+ unless organization_roles(config).key?(entry) || organization_role_by_name(ctx, organization["id"], entry)
318
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND"))
319
+ end
320
+ end
321
+ existing_member = find_member_by_email(ctx, organization["id"], email)
322
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION")) if existing_member
323
+ pending = ctx.context.adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization["id"]}, {field: "email", value: email}, {field: "status", value: "pending"}])
324
+ if pending.any?
325
+ if config[:cancel_pending_invitations_on_re_invite]
326
+ pending.each { |entry| ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: entry["id"]}], update: {status: "canceled"}) }
327
+ else
328
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION"))
329
+ end
330
+ end
331
+ pending_count = ctx.context.adapter.count(model: "invitation", where: [{field: "organizationId", value: organization["id"]}, {field: "status", value: "pending"}])
332
+ limit = config[:invitation_limit]
333
+ if limit && pending_count >= limit.to_i
334
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_LIMIT_REACHED"))
335
+ end
336
+ team_ids = organization_team_ids(body[:team_id] || body[:team_ids])
337
+ invitation = ctx.context.adapter.create(
338
+ model: "invitation",
339
+ data: {
340
+ organizationId: organization["id"],
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
+ }
349
+ )
350
+ sender = config[:send_invitation_email]
351
+ 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)
352
+ ctx.json(invitation_wire(ctx, invitation))
353
+ end
354
+ end
355
+
356
+ def organization_accept_invitation_endpoint(config)
357
+ Endpoint.new(path: "/organization/accept-invitation", method: "POST") do |ctx|
358
+ session = Routes.current_session(ctx)
359
+ body = normalize_hash(ctx.body)
360
+ invitation = invitation_by_id(ctx, body[:invitation_id] || body[:id])
361
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
362
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION")) unless invitation["email"].to_s.downcase == session[:user]["email"].to_s.downcase
363
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation["status"] == "pending"
364
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) if invitation["expiresAt"] && Time.parse(invitation["expiresAt"].to_s) < Time.now
365
+ if config[:require_email_verification_on_invitation] && !session[:user]["emailVerified"]
366
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION"))
367
+ end
368
+ member = ctx.context.adapter.create(model: "member", data: {organizationId: invitation["organizationId"], userId: session[:user]["id"], role: invitation["role"], createdAt: Time.now})
369
+ organization_team_ids(invitation["teamId"]).each do |team_id|
370
+ ctx.context.adapter.create(model: "teamMember", data: {teamId: team_id, userId: session[:user]["id"], createdAt: Time.now})
371
+ end
372
+ updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "accepted"})
373
+ organization = organization_by_id(ctx, invitation["organizationId"])
374
+ 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)
375
+ ctx.json({invitation: invitation_wire(ctx, updated), member: member_wire(ctx, member)})
376
+ end
377
+ end
378
+
379
+ def organization_reject_invitation_endpoint(_config)
380
+ Endpoint.new(path: "/organization/reject-invitation", method: "POST") do |ctx|
381
+ session = Routes.current_session(ctx)
382
+ invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
383
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
384
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION")) unless invitation["email"].to_s.downcase == session[:user]["email"].to_s.downcase
385
+ updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "rejected"})
386
+ ctx.json(invitation_wire(ctx, updated))
387
+ end
388
+ end
389
+
390
+ def organization_cancel_invitation_endpoint(config)
391
+ Endpoint.new(path: "/organization/cancel-invitation", method: "POST") do |ctx|
392
+ session = Routes.current_session(ctx)
393
+ invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
394
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
395
+ require_org_permission!(ctx, config, session, invitation["organizationId"], {invitation: ["cancel"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION"))
396
+ updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "canceled"})
397
+ ctx.json(invitation_wire(ctx, updated))
398
+ end
399
+ end
400
+
401
+ def organization_get_invitation_endpoint
402
+ Endpoint.new(path: "/organization/get-invitation", method: "GET") do |ctx|
403
+ invitation = invitation_by_id(ctx, normalize_hash(ctx.query)[:id] || normalize_hash(ctx.query)[:invitation_id])
404
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
405
+ ctx.json(invitation_wire(ctx, invitation))
406
+ end
407
+ end
408
+
409
+ def organization_list_invitations_endpoint(config)
410
+ Endpoint.new(path: "/organization/list-invitations", method: "GET") do |ctx|
411
+ session = Routes.current_session(ctx)
412
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
413
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
414
+ require_org_permission!(ctx, config, session, organization_id, {invitation: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION"))
415
+ invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization_id}])
416
+ ctx.json(invitations.map { |entry| invitation_wire(ctx, entry) })
417
+ end
418
+ end
419
+
420
+ def organization_list_user_invitations_endpoint
421
+ Endpoint.new(path: "/organization/list-user-invitations", method: "GET") do |ctx|
422
+ session = Routes.current_session(ctx)
423
+ invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "email", value: session[:user]["email"].to_s.downcase}, {field: "status", value: "pending"}])
424
+ ctx.json(invitations.map { |entry| invitation_wire(ctx, entry) })
425
+ end
426
+ end
427
+
428
+ def organization_add_member_endpoint(config)
429
+ Endpoint.new(path: "/organization/add-member", method: "POST") do |ctx|
430
+ session = Routes.current_session(ctx)
431
+ body = normalize_hash(ctx.body)
432
+ organization_id = body[:organization_id]
433
+ require_org_permission!(ctx, config, session, organization_id, {member: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_MEMBERSHIP_LIMIT_REACHED"))
434
+ user_id = body[:user_id].to_s
435
+ raise APIError.new("BAD_REQUEST", message: "userId is required") if user_id.empty?
436
+ if require_member(ctx, user_id, organization_id)
437
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION"))
438
+ end
439
+ organization = organization_by_id(ctx, organization_id)
440
+ user = ctx.context.internal_adapter.find_user_by_id(user_id)
441
+ 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))
442
+ merge_hook_data!(member_data, run_org_hook(config, :before_add_member, {member: member_data, user: user, organization: organization_wire(ctx, organization)}, ctx))
443
+ member = ctx.context.adapter.create(model: "member", data: member_data)
444
+ run_org_hook(config, :after_add_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
445
+ ctx.json(member_wire(ctx, member))
446
+ end
447
+ end
448
+
449
+ def organization_remove_member_endpoint(config)
450
+ Endpoint.new(path: "/organization/remove-member", method: "POST") do |ctx|
451
+ session = Routes.current_session(ctx)
452
+ body = normalize_hash(ctx.body)
453
+ member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
454
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless member
455
+ require_org_permission!(ctx, config, session, member["organizationId"], {member: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER"))
456
+ ensure_not_last_owner!(ctx, member)
457
+ organization = organization_by_id(ctx, member["organizationId"])
458
+ user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
459
+ ctx.context.adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
460
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "userId", value: member["userId"]}]) if org_truthy?(config.dig(:teams, :enabled))
461
+ run_org_hook(config, :after_remove_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
462
+ ctx.json({status: true})
463
+ end
464
+ end
465
+
466
+ def organization_update_member_role_endpoint(config)
467
+ Endpoint.new(path: "/organization/update-member-role", method: "POST") do |ctx|
468
+ session = Routes.current_session(ctx)
469
+ body = normalize_hash(ctx.body)
470
+ member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
471
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless member
472
+ require_org_permission!(ctx, config, session, member["organizationId"], {member: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER"))
473
+ updated = ctx.context.adapter.update(model: "member", where: [{field: "id", value: member["id"]}], update: {role: parse_roles(body[:role])})
474
+ ctx.json(member_wire(ctx, updated))
475
+ end
476
+ end
477
+
478
+ def organization_get_active_member_endpoint(_config)
479
+ Endpoint.new(path: "/organization/get-active-member", method: "GET") do |ctx|
480
+ session = Routes.current_session(ctx)
481
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
482
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
483
+ member = require_member!(ctx, session[:user]["id"], organization_id)
484
+ ctx.json(member_wire(ctx, member))
485
+ end
486
+ end
487
+
488
+ def organization_get_active_member_role_endpoint(_config)
489
+ Endpoint.new(path: "/organization/get-active-member-role", method: "GET") do |ctx|
490
+ session = Routes.current_session(ctx)
491
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
492
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
493
+ member = require_member!(ctx, session[:user]["id"], organization_id)
494
+ ctx.json({role: member["role"]})
495
+ end
496
+ end
497
+
498
+ def organization_leave_endpoint(config)
499
+ Endpoint.new(path: "/organization/leave", method: "POST") do |ctx|
500
+ session = Routes.current_session(ctx)
501
+ organization_id = normalize_hash(ctx.body)[:organization_id]
502
+ member = require_member!(ctx, session[:user]["id"], organization_id)
503
+ ensure_not_last_owner!(ctx, member)
504
+ ctx.context.adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
505
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}]) if org_truthy?(config.dig(:teams, :enabled))
506
+ ctx.json({status: true})
507
+ end
508
+ end
509
+
510
+ def organization_list_members_endpoint(_config)
511
+ Endpoint.new(path: "/organization/list-members", method: "GET") do |ctx|
512
+ session = Routes.current_session(ctx)
513
+ query = normalize_hash(ctx.query)
514
+ organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id")
515
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
516
+ require_member!(ctx, session[:user]["id"], organization_id)
517
+ ctx.json(list_members_for(ctx, organization_id, query))
518
+ end
519
+ end
520
+
521
+ def organization_has_permission_endpoint(config)
522
+ Endpoint.new(path: "/organization/has-permission", method: "POST") do |ctx|
523
+ session = Routes.current_session(ctx)
524
+ body = normalize_hash(ctx.body)
525
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
526
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
527
+ member = require_member!(ctx, session[:user]["id"], organization_id)
528
+ permissions = body[:permissions] || body[:permission]
529
+ ctx.json({error: nil, success: organization_permission?(ctx, config, member["role"], permissions, organization_id)})
530
+ end
531
+ end
532
+
533
+ def organization_create_team_endpoint(config)
534
+ Endpoint.new(path: "/organization/create-team", method: "POST") do |ctx|
535
+ session = Routes.current_session(ctx)
536
+ body = normalize_hash(ctx.body)
537
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
538
+ require_org_permission!(ctx, config, session, organization_id, {team: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION"))
539
+ organization = organization_by_id(ctx, organization_id)
540
+ max_teams = config.dig(:teams, :maximum_teams)
541
+ if max_teams && ctx.context.adapter.count(model: "team", where: [{field: "organizationId", value: organization_id}]) >= max_teams.to_i
542
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS"))
543
+ end
544
+ team_data = {organizationId: organization_id, name: body[:name].to_s, createdAt: Time.now}.merge(additional_input(body, :organization_id, :name))
545
+ 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)
547
+ ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
548
+ run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
549
+ ctx.json(team_wire(ctx, team))
550
+ end
551
+ end
552
+
553
+ def organization_list_teams_endpoint(_config)
554
+ Endpoint.new(path: "/organization/list-teams", method: "GET") do |ctx|
555
+ session = Routes.current_session(ctx)
556
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
557
+ require_member!(ctx, session[:user]["id"], organization_id)
558
+ teams = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: organization_id}])
559
+ ctx.json(teams.map { |team| team_wire(ctx, team) })
560
+ end
561
+ end
562
+
563
+ def organization_update_team_endpoint(config)
564
+ Endpoint.new(path: "/organization/update-team", method: "POST") do |ctx|
565
+ session = Routes.current_session(ctx)
566
+ body = normalize_hash(ctx.body)
567
+ team = team_by_id(ctx, body[:team_id])
568
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
569
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM"))
570
+ updated = ctx.context.adapter.update(model: "team", where: [{field: "id", value: team["id"]}], update: additional_input(body, :team_id, :organization_id))
571
+ ctx.json(team_wire(ctx, updated))
572
+ end
573
+ end
574
+
575
+ def organization_remove_team_endpoint(config)
576
+ Endpoint.new(path: "/organization/remove-team", method: "POST") do |ctx|
577
+ session = Routes.current_session(ctx)
578
+ team = team_by_id(ctx, normalize_hash(ctx.body)[:team_id])
579
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
580
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM"))
581
+ teams = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: team["organizationId"]}])
582
+ if teams.length <= 1 && config.dig(:teams, :allow_removing_all_teams) != true
583
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("UNABLE_TO_REMOVE_LAST_TEAM"))
584
+ end
585
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team["id"]}])
586
+ ctx.context.adapter.delete(model: "team", where: [{field: "id", value: team["id"]}])
587
+ ctx.json({status: true})
588
+ end
589
+ end
590
+
591
+ def organization_set_active_team_endpoint(_config)
592
+ Endpoint.new(path: "/organization/set-active-team", method: "POST") do |ctx|
593
+ session = Routes.current_session(ctx)
594
+ body = normalize_hash(ctx.body)
595
+ if body.key?(:team_id) && body[:team_id].nil?
596
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeTeamId: nil})
597
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeTeamId" => nil), user: session[:user]})
598
+ next ctx.json({status: true})
599
+ end
600
+ team = team_by_id(ctx, body[:team_id])
601
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
602
+ require_team_member!(ctx, session[:user]["id"], team["id"])
603
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeOrganizationId: team["organizationId"], activeTeamId: team["id"]})
604
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeOrganizationId" => team["organizationId"], "activeTeamId" => team["id"]), user: session[:user]})
605
+ ctx.json(team_wire(ctx, team))
606
+ end
607
+ end
608
+
609
+ def organization_list_user_teams_endpoint
610
+ Endpoint.new(path: "/organization/list-user-teams", method: "GET") do |ctx|
611
+ session = Routes.current_session(ctx)
612
+ memberships = ctx.context.adapter.find_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}])
613
+ ctx.json(memberships.filter_map { |entry| team_by_id(ctx, entry["teamId"]) }.map { |team| team_wire(ctx, team) })
614
+ end
615
+ end
616
+
617
+ def organization_list_team_members_endpoint(_config)
618
+ Endpoint.new(path: "/organization/list-team-members", method: "GET") do |ctx|
619
+ session = Routes.current_session(ctx)
620
+ team_id = normalize_hash(ctx.query)[:team_id] || session[:session]["activeTeamId"]
621
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM")) unless team_id
622
+ team = team_by_id(ctx, team_id)
623
+ require_member!(ctx, session[:user]["id"], team["organizationId"])
624
+ members = ctx.context.adapter.find_many(model: "teamMember", where: [{field: "teamId", value: team_id}])
625
+ ctx.json(members.map { |entry| team_member_wire(ctx, entry) })
626
+ end
627
+ end
628
+
629
+ def organization_add_team_member_endpoint(config)
630
+ Endpoint.new(path: "/organization/add-team-member", method: "POST") do |ctx|
631
+ session = Routes.current_session(ctx)
632
+ body = normalize_hash(ctx.body)
633
+ team = team_by_id(ctx, body[:team_id])
634
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
635
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER"))
636
+ user_id = body[:user_id].to_s
637
+ require_member!(ctx, user_id, team["organizationId"])
638
+ max_members = config.dig(:teams, :maximum_members_per_team)
639
+ if max_members && ctx.context.adapter.count(model: "teamMember", where: [{field: "teamId", value: team["id"]}]) >= max_members.to_i
640
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_MEMBER_LIMIT_REACHED"))
641
+ end
642
+ existing = ctx.context.adapter.find_one(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: user_id}])
643
+ next ctx.json(team_member_wire(ctx, existing)) if existing
644
+
645
+ member = ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: user_id, createdAt: Time.now})
646
+ ctx.json(team_member_wire(ctx, member))
647
+ end
648
+ end
649
+
650
+ def organization_remove_team_member_endpoint(config)
651
+ Endpoint.new(path: "/organization/remove-team-member", method: "POST") do |ctx|
652
+ session = Routes.current_session(ctx)
653
+ body = normalize_hash(ctx.body)
654
+ team = team_by_id(ctx, body[:team_id])
655
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
656
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER"))
657
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: body[:user_id]}])
658
+ ctx.json({status: true})
659
+ end
660
+ end
661
+
662
+ def organization_create_role_endpoint(config)
663
+ Endpoint.new(path: "/organization/create-role", method: "POST") do |ctx|
664
+ session = Routes.current_session(ctx)
665
+ body = normalize_hash(ctx.body)
666
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
667
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE"))
668
+ role_name = (body[:role] || body[:role_name]).to_s
669
+ 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)
670
+ permission = stringify_permission(body[:permission] || body[:permissions])
671
+ validate_permission_resources!(config, permission)
672
+ unless organization_permission?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
673
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE"))
674
+ end
675
+ 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)))
676
+ wired = organization_role_wire(role)
677
+ ctx.json({success: true, roleData: wired, statements: (config[:ac] || create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)).new_role(permission).statements})
678
+ end
679
+ end
680
+
681
+ def organization_list_roles_endpoint(config)
682
+ Endpoint.new(path: "/organization/list-roles", method: "GET") do |ctx|
683
+ session = Routes.current_session(ctx)
684
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
685
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["read"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE"))
686
+ defaults = organization_roles(config).keys.map { |role| {"role" => role, "permission" => {}} }
687
+ dynamic = ctx.context.adapter.find_many(model: "organizationRole", where: [{field: "organizationId", value: organization_id}]).map { |role| organization_role_wire(role) }
688
+ ctx.json(defaults + dynamic)
689
+ end
690
+ end
691
+
692
+ def organization_get_role_endpoint(config)
693
+ Endpoint.new(path: "/organization/get-role", method: "GET") do |ctx|
694
+ session = Routes.current_session(ctx)
695
+ query = normalize_hash(ctx.query)
696
+ organization_id = query[:organization_id] || session[:session]["activeOrganizationId"]
697
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["read"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE"))
698
+ role = organization_role_by_id(ctx, query[:role_id]) || organization_role_by_name(ctx, organization_id, query[:role])
699
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND")) unless role
700
+ ctx.json(organization_role_wire(role))
701
+ end
702
+ end
703
+
704
+ def organization_update_role_endpoint(config)
705
+ Endpoint.new(path: "/organization/update-role", method: "POST") do |ctx|
706
+ session = Routes.current_session(ctx)
707
+ body = normalize_hash(ctx.body)
708
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
709
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE"))
710
+ role = organization_role_by_id(ctx, body[:role_id]) || organization_role_by_name(ctx, organization_id, body[:role] || body[:role_name])
711
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND")) unless role
712
+ update = {}
713
+ update[:role] = body[:data][:role] || body[:data][:role_name] if body[:data].is_a?(Hash) && (body[:data][:role] || body[:data][:role_name])
714
+ permission = body[:permission] || body[:permissions] || body.dig(:data, :permission) || body.dig(:data, :permissions)
715
+ if permission
716
+ permission = stringify_permission(permission)
717
+ validate_permission_resources!(config, permission)
718
+ unless organization_permission?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
719
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE"))
720
+ end
721
+ update[:permission] = JSON.generate(permission)
722
+ end
723
+ update.merge!(additional_input(body[:data], :role, :role_name, :permission, :permissions)) if body[:data].is_a?(Hash)
724
+ updated = ctx.context.adapter.update(model: "organizationRole", where: [{field: "id", value: role["id"]}], update: update)
725
+ ctx.json({success: true, roleData: organization_role_wire(updated)})
726
+ end
727
+ end
728
+
729
+ def organization_delete_role_endpoint(config)
730
+ Endpoint.new(path: "/organization/delete-role", method: "POST") do |ctx|
731
+ session = Routes.current_session(ctx)
732
+ body = normalize_hash(ctx.body)
733
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
734
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE"))
735
+ role_name = body[:role] || body[:role_name]
736
+ if role_name && organization_roles(config).key?(role_name.to_s)
737
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("CANNOT_DELETE_A_PRE_DEFINED_ROLE"))
738
+ end
739
+ role = organization_role_by_id(ctx, body[:role_id]) || organization_role_by_name(ctx, organization_id, role_name)
740
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND")) unless role
741
+ assigned = ctx.context.adapter.find_many(model: "member", where: [{field: "organizationId", value: organization_id}]).any? do |member|
742
+ member["role"].to_s.split(",").map(&:strip).include?(role["role"])
743
+ end
744
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_IS_ASSIGNED_TO_MEMBERS")) if assigned
745
+ ctx.context.adapter.delete(model: "organizationRole", where: [{field: "id", value: role["id"]}])
746
+ ctx.json({success: true})
747
+ end
748
+ end
749
+
750
+ def parse_roles(roles)
751
+ Array(roles).join(",")
752
+ end
753
+
754
+ def organization_roles(config)
755
+ (config[:roles] || organization_default_roles(config)).transform_keys(&:to_s)
756
+ end
757
+
758
+ def organization_permission?(ctx, config, role_string, permissions, organization_id)
759
+ roles = organization_roles(config)
760
+ if org_truthy?(config.dig(:dynamic_access_control, :enabled))
761
+ ctx.context.adapter.find_many(model: "organizationRole", where: [{field: "organizationId", value: organization_id}]).each do |entry|
762
+ permission = parse_permission(entry["permission"])
763
+ if roles.key?(entry["role"])
764
+ permission = merge_permissions(roles[entry["role"]].statements, permission)
765
+ end
766
+ roles[entry["role"]] = (config[:ac] || create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)).new_role(permission)
767
+ end
768
+ end
769
+ role_string.to_s.split(",").any? do |role|
770
+ roles[role]&.authorize(permissions || {})&.fetch(:success, false)
771
+ end
772
+ end
773
+
774
+ def require_org_permission!(ctx, config, session, organization_id, permissions, message)
775
+ member = require_member!(ctx, session[:user]["id"], organization_id)
776
+ return member if organization_permission?(ctx, config, member["role"], permissions, organization_id)
777
+
778
+ raise APIError.new("FORBIDDEN", message: message)
779
+ end
780
+
781
+ def merge_permissions(base, extra)
782
+ stringify_permission(base).merge(stringify_permission(extra)) do |_resource, base_actions, extra_actions|
783
+ (Array(base_actions) + Array(extra_actions)).map(&:to_s).uniq
784
+ end
785
+ end
786
+
787
+ def require_member!(ctx, user_id, organization_id)
788
+ member = require_member(ctx, user_id, organization_id)
789
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION")) unless member
790
+
791
+ member
792
+ end
793
+
794
+ def require_member(ctx, user_id, organization_id)
795
+ return nil if user_id.to_s.empty? || organization_id.to_s.empty?
796
+
797
+ ctx.context.adapter.find_one(model: "member", where: [{field: "userId", value: user_id}, {field: "organizationId", value: organization_id}])
798
+ end
799
+
800
+ def require_team_member!(ctx, user_id, team_id)
801
+ member = ctx.context.adapter.find_one(model: "teamMember", where: [{field: "userId", value: user_id}, {field: "teamId", value: team_id}])
802
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_NOT_A_MEMBER_OF_THE_TEAM")) unless member
803
+
804
+ member
805
+ end
806
+
807
+ def member_by_id(ctx, id)
808
+ return nil if id.to_s.empty?
809
+
810
+ ctx.context.adapter.find_one(model: "member", where: [{field: "id", value: id}])
811
+ end
812
+
813
+ def find_member_by_email(ctx, organization_id, email)
814
+ user = ctx.context.adapter.find_one(model: "user", where: [{field: "email", value: email.to_s.downcase}])
815
+ user && require_member(ctx, user["id"], organization_id)
816
+ end
817
+
818
+ def organization_by_id(ctx, id)
819
+ return nil if id.to_s.empty?
820
+
821
+ ctx.context.adapter.find_one(model: "organization", where: [{field: "id", value: id}])
822
+ end
823
+
824
+ def organization_by_slug(ctx, slug)
825
+ return nil if slug.to_s.empty?
826
+
827
+ ctx.context.adapter.find_one(model: "organization", where: [{field: "slug", value: slug}])
828
+ end
829
+
830
+ def invitation_by_id(ctx, id)
831
+ return nil if id.to_s.empty?
832
+
833
+ ctx.context.adapter.find_one(model: "invitation", where: [{field: "id", value: id}])
834
+ end
835
+
836
+ def team_by_id(ctx, id)
837
+ return nil if id.to_s.empty?
838
+
839
+ ctx.context.adapter.find_one(model: "team", where: [{field: "id", value: id}])
840
+ end
841
+
842
+ def organization_role_by_id(ctx, id)
843
+ return nil if id.to_s.empty?
844
+
845
+ ctx.context.adapter.find_one(model: "organizationRole", where: [{field: "id", value: id}])
846
+ end
847
+
848
+ def organization_role_by_name(ctx, organization_id, role)
849
+ return nil if role.to_s.empty?
850
+
851
+ ctx.context.adapter.find_one(model: "organizationRole", where: [{field: "organizationId", value: organization_id}, {field: "role", value: role}])
852
+ end
853
+
854
+ def list_members_for(ctx, organization_id, query = {})
855
+ where = [{field: "organizationId", value: organization_id}]
856
+ if query[:filter_field]
857
+ where << {field: query[:filter_field], value: query[:filter_value], operator: query[:filter_operator]}
858
+ elsif query[:filter].is_a?(Hash)
859
+ filter = normalize_hash(query[:filter])
860
+ where << {field: filter[:field], value: filter[:value], operator: filter[:operator]}
861
+ end
862
+ members = ctx.context.adapter.find_many(
863
+ model: "member",
864
+ where: where,
865
+ limit: query[:limit],
866
+ offset: query[:offset],
867
+ sort_by: query[:sort_by] ? {field: query[:sort_by], direction: query[:sort_order] || "asc"} : nil
868
+ )
869
+ {
870
+ members: members.map { |entry| member_wire(ctx, entry) },
871
+ total: ctx.context.adapter.count(model: "member", where: where)
872
+ }
873
+ end
874
+
875
+ def member_wire(ctx, member)
876
+ data = Schema.parse_output(ctx.context.options, "member", member)
877
+ user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
878
+ data["user"] = user.slice("id", "name", "email", "image") if user
879
+ data
880
+ end
881
+
882
+ def organization_wire(ctx, organization)
883
+ data = Schema.parse_output(ctx.context.options, "organization", organization)
884
+ data["metadata"] = parse_metadata(data["metadata"]) if data&.key?("metadata")
885
+ data
886
+ end
887
+
888
+ def invitation_wire(ctx, invitation)
889
+ Schema.parse_output(ctx.context.options, "invitation", invitation)
890
+ end
891
+
892
+ def team_wire(ctx, team)
893
+ Schema.parse_output(ctx.context.options, "team", team)
894
+ end
895
+
896
+ def team_member_wire(ctx, member)
897
+ Schema.parse_output(ctx.context.options, "teamMember", member)
898
+ end
899
+
900
+ def organization_role_wire(role)
901
+ role.merge("permission" => parse_permission(role["permission"]))
902
+ end
903
+
904
+ def ensure_not_last_owner!(ctx, member)
905
+ return unless member["role"].to_s.split(",").include?("owner")
906
+
907
+ owners = ctx.context.adapter.find_many(model: "member", where: [{field: "organizationId", value: member["organizationId"]}]).select { |entry| entry["role"].to_s.split(",").include?("owner") }
908
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER")) if owners.length <= 1
909
+ end
910
+
911
+ def create_default_team(ctx, config, organization, session)
912
+ custom = config.dig(:teams, :default_team, :custom_create_default_team)
913
+ team_data = {organizationId: organization["id"], name: organization["name"], createdAt: Time.now}
914
+ merge_hook_data!(team_data, run_org_hook(config, :before_create_team, {team: team_data, user: session[:user], organization: organization_wire(ctx, organization)}, ctx))
915
+ team = if custom.respond_to?(:call)
916
+ custom.call(organization_wire(ctx, organization), ctx)
917
+ else
918
+ ctx.context.adapter.create(model: "team", data: team_data)
919
+ end
920
+ ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
921
+ run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
922
+ team
923
+ end
924
+
925
+ def organization_created_count(ctx, user_id)
926
+ members = ctx.context.adapter.find_many(model: "member", where: [{field: "userId", value: user_id}])
927
+ members.count { |member| member["role"].to_s.split(",").include?("owner") }
928
+ end
929
+
930
+ def run_org_hook(config, key, data, ctx)
931
+ hooks = [config.dig(:organization_hooks, key), config.dig(:hooks, key)]
932
+ hooks.concat(ctx.context.options.plugins.filter_map { |plugin| plugin.dig(:options, :organization_hooks, key) || plugin.dig("options", "organizationHooks", key.to_s) }) if ctx&.context&.options
933
+ 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) }
934
+ end
935
+
936
+ def merge_hook_data!(target, response)
937
+ data = if response.is_a?(Hash)
938
+ normalize_hash(response)[:data]
939
+ end
940
+ target.merge!(normalize_hash(data)) if data.is_a?(Hash)
941
+ target
942
+ end
943
+
944
+ def parse_metadata(value)
945
+ return value if value.nil? || value.is_a?(Hash)
946
+
947
+ JSON.parse(value)
948
+ rescue JSON::ParserError
949
+ value
950
+ end
951
+
952
+ def serialize_metadata(value)
953
+ value.is_a?(Hash) ? JSON.generate(value) : value
954
+ end
955
+
956
+ def parse_permission(value)
957
+ return value if value.is_a?(Hash)
958
+ return {} if value.nil? || value.to_s.empty?
959
+
960
+ JSON.parse(value)
961
+ rescue JSON::ParserError
962
+ {}
963
+ end
964
+
965
+ def stringify_permission(value)
966
+ normalize_hash(value || {}).each_with_object({}) do |(resource, actions), result|
967
+ result[resource.to_s] = Array(actions).map(&:to_s)
968
+ end
969
+ end
970
+
971
+ def validate_permission_resources!(config, permission)
972
+ valid = (config[:ac] || create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)).statements.keys
973
+ invalid = permission.keys - valid
974
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVALID_RESOURCE")) if invalid.any?
975
+ end
976
+
977
+ def organization_team_ids(value)
978
+ Array(value).flat_map { |entry| entry.to_s.split(",") }.map(&:strip).reject(&:empty?)
979
+ end
980
+
981
+ def additional_input(hash, *exclude)
982
+ data = normalize_hash(hash)
983
+ additional = normalize_hash(data.delete(:additional_fields))
984
+ extra_input(data, *exclude, :additional_fields).merge(additional)
985
+ end
986
+
987
+ def extra_input(hash, *exclude)
988
+ normalize_hash(hash).except(*exclude.map(&:to_sym))
989
+ end
990
+
991
+ def org_truthy?(value)
992
+ value == true || value.to_s == "true"
993
+ end
994
+ end
995
+ end