better_auth 0.1.1 → 0.2.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +106 -16
  4. data/lib/better_auth/adapters/base.rb +49 -0
  5. data/lib/better_auth/adapters/internal_adapter.rb +439 -0
  6. data/lib/better_auth/adapters/memory.rb +232 -0
  7. data/lib/better_auth/adapters/mongodb.rb +369 -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 +425 -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 +210 -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 +129 -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 +348 -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 +990 -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 +215 -0
  68. data/lib/better_auth/request_ip.rb +70 -0
  69. data/lib/better_auth/router.rb +365 -0
  70. data/lib/better_auth/routes/account.rb +211 -0
  71. data/lib/better_auth/routes/email_verification.rb +108 -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 +164 -0
  75. data/lib/better_auth/routes/session.rb +137 -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 +145 -0
  79. data/lib/better_auth/routes/social.rb +188 -0
  80. data/lib/better_auth/routes/user.rb +193 -0
  81. data/lib/better_auth/schema/sql.rb +191 -0
  82. data/lib/better_auth/schema.rb +275 -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 +55 -0
  86. data/lib/better_auth/social_providers/base.rb +67 -0
  87. data/lib/better_auth/social_providers/discord.rb +59 -0
  88. data/lib/better_auth/social_providers/github.rb +59 -0
  89. data/lib/better_auth/social_providers/gitlab.rb +54 -0
  90. data/lib/better_auth/social_providers/google.rb +65 -0
  91. data/lib/better_auth/social_providers/microsoft_entra_id.rb +65 -0
  92. data/lib/better_auth/social_providers.rb +9 -0
  93. data/lib/better_auth/version.rb +1 -1
  94. data/lib/better_auth.rb +87 -2
  95. metadata +218 -21
  96. data/.ruby-version +0 -1
  97. data/.standard.yml +0 -12
  98. data/.vscode/settings.json +0 -22
  99. data/AGENTS.md +0 -50
  100. data/CLAUDE.md +0 -1
  101. data/CODE_OF_CONDUCT.md +0 -173
  102. data/CONTRIBUTING.md +0 -187
  103. data/Gemfile +0 -12
  104. data/Makefile +0 -207
  105. data/Rakefile +0 -25
  106. data/SECURITY.md +0 -28
  107. data/docker-compose.yml +0 -63
@@ -0,0 +1,990 @@
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
+ ctx.json({invitation: invitation_wire(ctx, updated), member: member_wire(ctx, member)})
374
+ end
375
+ end
376
+
377
+ def organization_reject_invitation_endpoint(_config)
378
+ Endpoint.new(path: "/organization/reject-invitation", method: "POST") do |ctx|
379
+ session = Routes.current_session(ctx)
380
+ invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
381
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
382
+ 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
383
+ updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "rejected"})
384
+ ctx.json(invitation_wire(ctx, updated))
385
+ end
386
+ end
387
+
388
+ def organization_cancel_invitation_endpoint(config)
389
+ Endpoint.new(path: "/organization/cancel-invitation", method: "POST") do |ctx|
390
+ session = Routes.current_session(ctx)
391
+ invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
392
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
393
+ require_org_permission!(ctx, config, session, invitation["organizationId"], {invitation: ["cancel"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION"))
394
+ updated = ctx.context.adapter.update(model: "invitation", where: [{field: "id", value: invitation["id"]}], update: {status: "canceled"})
395
+ ctx.json(invitation_wire(ctx, updated))
396
+ end
397
+ end
398
+
399
+ def organization_get_invitation_endpoint
400
+ Endpoint.new(path: "/organization/get-invitation", method: "GET") do |ctx|
401
+ invitation = invitation_by_id(ctx, normalize_hash(ctx.query)[:id] || normalize_hash(ctx.query)[:invitation_id])
402
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
403
+ ctx.json(invitation_wire(ctx, invitation))
404
+ end
405
+ end
406
+
407
+ def organization_list_invitations_endpoint(config)
408
+ Endpoint.new(path: "/organization/list-invitations", method: "GET") do |ctx|
409
+ session = Routes.current_session(ctx)
410
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
411
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
412
+ require_org_permission!(ctx, config, session, organization_id, {invitation: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION"))
413
+ invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "organizationId", value: organization_id}])
414
+ ctx.json(invitations.map { |entry| invitation_wire(ctx, entry) })
415
+ end
416
+ end
417
+
418
+ def organization_list_user_invitations_endpoint
419
+ Endpoint.new(path: "/organization/list-user-invitations", method: "GET") do |ctx|
420
+ session = Routes.current_session(ctx)
421
+ invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "email", value: session[:user]["email"].to_s.downcase}, {field: "status", value: "pending"}])
422
+ ctx.json(invitations.map { |entry| invitation_wire(ctx, entry) })
423
+ end
424
+ end
425
+
426
+ def organization_add_member_endpoint(config)
427
+ Endpoint.new(path: "/organization/add-member", method: "POST") do |ctx|
428
+ session = Routes.current_session(ctx)
429
+ body = normalize_hash(ctx.body)
430
+ organization_id = body[:organization_id]
431
+ require_org_permission!(ctx, config, session, organization_id, {member: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_MEMBERSHIP_LIMIT_REACHED"))
432
+ user_id = body[:user_id].to_s
433
+ raise APIError.new("BAD_REQUEST", message: "userId is required") if user_id.empty?
434
+ if require_member(ctx, user_id, organization_id)
435
+ raise APIError.new("CONFLICT", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION"))
436
+ end
437
+ organization = organization_by_id(ctx, organization_id)
438
+ user = ctx.context.internal_adapter.find_user_by_id(user_id)
439
+ 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))
440
+ merge_hook_data!(member_data, run_org_hook(config, :before_add_member, {member: member_data, user: user, organization: organization_wire(ctx, organization)}, ctx))
441
+ member = ctx.context.adapter.create(model: "member", data: member_data)
442
+ run_org_hook(config, :after_add_member, {member: member_wire(ctx, member), user: user, organization: organization_wire(ctx, organization)}, ctx)
443
+ ctx.json(member_wire(ctx, member))
444
+ end
445
+ end
446
+
447
+ def organization_remove_member_endpoint(config)
448
+ Endpoint.new(path: "/organization/remove-member", method: "POST") do |ctx|
449
+ session = Routes.current_session(ctx)
450
+ body = normalize_hash(ctx.body)
451
+ member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
452
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless member
453
+ require_org_permission!(ctx, config, session, member["organizationId"], {member: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER"))
454
+ ensure_not_last_owner!(ctx, member)
455
+ ctx.context.adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
456
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "userId", value: member["userId"]}]) if org_truthy?(config.dig(:teams, :enabled))
457
+ ctx.json({status: true})
458
+ end
459
+ end
460
+
461
+ def organization_update_member_role_endpoint(config)
462
+ Endpoint.new(path: "/organization/update-member-role", method: "POST") do |ctx|
463
+ session = Routes.current_session(ctx)
464
+ body = normalize_hash(ctx.body)
465
+ member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
466
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("MEMBER_NOT_FOUND")) unless member
467
+ require_org_permission!(ctx, config, session, member["organizationId"], {member: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER"))
468
+ updated = ctx.context.adapter.update(model: "member", where: [{field: "id", value: member["id"]}], update: {role: parse_roles(body[:role])})
469
+ ctx.json(member_wire(ctx, updated))
470
+ end
471
+ end
472
+
473
+ def organization_get_active_member_endpoint(_config)
474
+ Endpoint.new(path: "/organization/get-active-member", method: "GET") do |ctx|
475
+ session = Routes.current_session(ctx)
476
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
477
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
478
+ member = require_member!(ctx, session[:user]["id"], organization_id)
479
+ ctx.json(member_wire(ctx, member))
480
+ end
481
+ end
482
+
483
+ def organization_get_active_member_role_endpoint(_config)
484
+ Endpoint.new(path: "/organization/get-active-member-role", method: "GET") do |ctx|
485
+ session = Routes.current_session(ctx)
486
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
487
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
488
+ member = require_member!(ctx, session[:user]["id"], organization_id)
489
+ ctx.json({role: member["role"]})
490
+ end
491
+ end
492
+
493
+ def organization_leave_endpoint(config)
494
+ Endpoint.new(path: "/organization/leave", method: "POST") do |ctx|
495
+ session = Routes.current_session(ctx)
496
+ organization_id = normalize_hash(ctx.body)[:organization_id]
497
+ member = require_member!(ctx, session[:user]["id"], organization_id)
498
+ ensure_not_last_owner!(ctx, member)
499
+ ctx.context.adapter.delete(model: "member", where: [{field: "id", value: member["id"]}])
500
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}]) if org_truthy?(config.dig(:teams, :enabled))
501
+ ctx.json({status: true})
502
+ end
503
+ end
504
+
505
+ def organization_list_members_endpoint(_config)
506
+ Endpoint.new(path: "/organization/list-members", method: "GET") do |ctx|
507
+ session = Routes.current_session(ctx)
508
+ query = normalize_hash(ctx.query)
509
+ organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id")
510
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
511
+ require_member!(ctx, session[:user]["id"], organization_id)
512
+ ctx.json(list_members_for(ctx, organization_id, query))
513
+ end
514
+ end
515
+
516
+ def organization_has_permission_endpoint(config)
517
+ Endpoint.new(path: "/organization/has-permission", method: "POST") do |ctx|
518
+ session = Routes.current_session(ctx)
519
+ body = normalize_hash(ctx.body)
520
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
521
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
522
+ member = require_member!(ctx, session[:user]["id"], organization_id)
523
+ permissions = body[:permissions] || body[:permission]
524
+ ctx.json({error: nil, success: organization_permission?(ctx, config, member["role"], permissions, organization_id)})
525
+ end
526
+ end
527
+
528
+ def organization_create_team_endpoint(config)
529
+ Endpoint.new(path: "/organization/create-team", method: "POST") do |ctx|
530
+ session = Routes.current_session(ctx)
531
+ body = normalize_hash(ctx.body)
532
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
533
+ require_org_permission!(ctx, config, session, organization_id, {team: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION"))
534
+ organization = organization_by_id(ctx, organization_id)
535
+ max_teams = config.dig(:teams, :maximum_teams)
536
+ if max_teams && ctx.context.adapter.count(model: "team", where: [{field: "organizationId", value: organization_id}]) >= max_teams.to_i
537
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS"))
538
+ end
539
+ team_data = {organizationId: organization_id, name: body[:name].to_s, createdAt: Time.now}.merge(additional_input(body, :organization_id, :name))
540
+ merge_hook_data!(team_data, run_org_hook(config, :before_create_team, {team: team_data, user: session[:user], organization: organization_wire(ctx, organization)}, ctx))
541
+ team = ctx.context.adapter.create(model: "team", data: team_data)
542
+ ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
543
+ run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
544
+ ctx.json(team_wire(ctx, team))
545
+ end
546
+ end
547
+
548
+ def organization_list_teams_endpoint(_config)
549
+ Endpoint.new(path: "/organization/list-teams", method: "GET") do |ctx|
550
+ session = Routes.current_session(ctx)
551
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
552
+ require_member!(ctx, session[:user]["id"], organization_id)
553
+ teams = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: organization_id}])
554
+ ctx.json(teams.map { |team| team_wire(ctx, team) })
555
+ end
556
+ end
557
+
558
+ def organization_update_team_endpoint(config)
559
+ Endpoint.new(path: "/organization/update-team", method: "POST") do |ctx|
560
+ session = Routes.current_session(ctx)
561
+ body = normalize_hash(ctx.body)
562
+ team = team_by_id(ctx, body[:team_id])
563
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
564
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM"))
565
+ updated = ctx.context.adapter.update(model: "team", where: [{field: "id", value: team["id"]}], update: additional_input(body, :team_id, :organization_id))
566
+ ctx.json(team_wire(ctx, updated))
567
+ end
568
+ end
569
+
570
+ def organization_remove_team_endpoint(config)
571
+ Endpoint.new(path: "/organization/remove-team", method: "POST") do |ctx|
572
+ session = Routes.current_session(ctx)
573
+ team = team_by_id(ctx, normalize_hash(ctx.body)[:team_id])
574
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
575
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM"))
576
+ teams = ctx.context.adapter.find_many(model: "team", where: [{field: "organizationId", value: team["organizationId"]}])
577
+ if teams.length <= 1 && config.dig(:teams, :allow_removing_all_teams) != true
578
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("UNABLE_TO_REMOVE_LAST_TEAM"))
579
+ end
580
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team["id"]}])
581
+ ctx.context.adapter.delete(model: "team", where: [{field: "id", value: team["id"]}])
582
+ ctx.json({status: true})
583
+ end
584
+ end
585
+
586
+ def organization_set_active_team_endpoint(_config)
587
+ Endpoint.new(path: "/organization/set-active-team", method: "POST") do |ctx|
588
+ session = Routes.current_session(ctx)
589
+ body = normalize_hash(ctx.body)
590
+ if body.key?(:team_id) && body[:team_id].nil?
591
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeTeamId: nil})
592
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeTeamId" => nil), user: session[:user]})
593
+ next ctx.json({status: true})
594
+ end
595
+ team = team_by_id(ctx, body[:team_id])
596
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
597
+ require_team_member!(ctx, session[:user]["id"], team["id"])
598
+ updated_session = ctx.context.internal_adapter.update_session(session[:session]["token"], {activeOrganizationId: team["organizationId"], activeTeamId: team["id"]})
599
+ Cookies.set_session_cookie(ctx, {session: updated_session || session[:session].merge("activeOrganizationId" => team["organizationId"], "activeTeamId" => team["id"]), user: session[:user]})
600
+ ctx.json(team_wire(ctx, team))
601
+ end
602
+ end
603
+
604
+ def organization_list_user_teams_endpoint
605
+ Endpoint.new(path: "/organization/list-user-teams", method: "GET") do |ctx|
606
+ session = Routes.current_session(ctx)
607
+ memberships = ctx.context.adapter.find_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}])
608
+ ctx.json(memberships.filter_map { |entry| team_by_id(ctx, entry["teamId"]) }.map { |team| team_wire(ctx, team) })
609
+ end
610
+ end
611
+
612
+ def organization_list_team_members_endpoint(_config)
613
+ Endpoint.new(path: "/organization/list-team-members", method: "GET") do |ctx|
614
+ session = Routes.current_session(ctx)
615
+ team_id = normalize_hash(ctx.query)[:team_id] || session[:session]["activeTeamId"]
616
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM")) unless team_id
617
+ team = team_by_id(ctx, team_id)
618
+ require_member!(ctx, session[:user]["id"], team["organizationId"])
619
+ members = ctx.context.adapter.find_many(model: "teamMember", where: [{field: "teamId", value: team_id}])
620
+ ctx.json(members.map { |entry| team_member_wire(ctx, entry) })
621
+ end
622
+ end
623
+
624
+ def organization_add_team_member_endpoint(config)
625
+ Endpoint.new(path: "/organization/add-team-member", method: "POST") do |ctx|
626
+ session = Routes.current_session(ctx)
627
+ body = normalize_hash(ctx.body)
628
+ team = team_by_id(ctx, body[:team_id])
629
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
630
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER"))
631
+ user_id = body[:user_id].to_s
632
+ require_member!(ctx, user_id, team["organizationId"])
633
+ max_members = config.dig(:teams, :maximum_members_per_team)
634
+ if max_members && ctx.context.adapter.count(model: "teamMember", where: [{field: "teamId", value: team["id"]}]) >= max_members.to_i
635
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_MEMBER_LIMIT_REACHED"))
636
+ end
637
+ existing = ctx.context.adapter.find_one(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: user_id}])
638
+ next ctx.json(team_member_wire(ctx, existing)) if existing
639
+
640
+ member = ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: user_id, createdAt: Time.now})
641
+ ctx.json(team_member_wire(ctx, member))
642
+ end
643
+ end
644
+
645
+ def organization_remove_team_member_endpoint(config)
646
+ Endpoint.new(path: "/organization/remove-team-member", method: "POST") do |ctx|
647
+ session = Routes.current_session(ctx)
648
+ body = normalize_hash(ctx.body)
649
+ team = team_by_id(ctx, body[:team_id])
650
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
651
+ require_org_permission!(ctx, config, session, team["organizationId"], {team: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER"))
652
+ ctx.context.adapter.delete_many(model: "teamMember", where: [{field: "teamId", value: team["id"]}, {field: "userId", value: body[:user_id]}])
653
+ ctx.json({status: true})
654
+ end
655
+ end
656
+
657
+ def organization_create_role_endpoint(config)
658
+ Endpoint.new(path: "/organization/create-role", method: "POST") do |ctx|
659
+ session = Routes.current_session(ctx)
660
+ body = normalize_hash(ctx.body)
661
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
662
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["create"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE"))
663
+ role_name = (body[:role] || body[:role_name]).to_s
664
+ 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)
665
+ permission = stringify_permission(body[:permission] || body[:permissions])
666
+ validate_permission_resources!(config, permission)
667
+ unless organization_permission?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
668
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE"))
669
+ end
670
+ 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)))
671
+ wired = organization_role_wire(role)
672
+ ctx.json({success: true, roleData: wired, statements: (config[:ac] || create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)).new_role(permission).statements})
673
+ end
674
+ end
675
+
676
+ def organization_list_roles_endpoint(config)
677
+ Endpoint.new(path: "/organization/list-roles", method: "GET") do |ctx|
678
+ session = Routes.current_session(ctx)
679
+ organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
680
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["read"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE"))
681
+ defaults = organization_roles(config).keys.map { |role| {"role" => role, "permission" => {}} }
682
+ dynamic = ctx.context.adapter.find_many(model: "organizationRole", where: [{field: "organizationId", value: organization_id}]).map { |role| organization_role_wire(role) }
683
+ ctx.json(defaults + dynamic)
684
+ end
685
+ end
686
+
687
+ def organization_get_role_endpoint(config)
688
+ Endpoint.new(path: "/organization/get-role", method: "GET") do |ctx|
689
+ session = Routes.current_session(ctx)
690
+ query = normalize_hash(ctx.query)
691
+ organization_id = query[:organization_id] || session[:session]["activeOrganizationId"]
692
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["read"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE"))
693
+ role = organization_role_by_id(ctx, query[:role_id]) || organization_role_by_name(ctx, organization_id, query[:role])
694
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND")) unless role
695
+ ctx.json(organization_role_wire(role))
696
+ end
697
+ end
698
+
699
+ def organization_update_role_endpoint(config)
700
+ Endpoint.new(path: "/organization/update-role", method: "POST") do |ctx|
701
+ session = Routes.current_session(ctx)
702
+ body = normalize_hash(ctx.body)
703
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
704
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["update"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE"))
705
+ role = organization_role_by_id(ctx, body[:role_id]) || organization_role_by_name(ctx, organization_id, body[:role] || body[:role_name])
706
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND")) unless role
707
+ update = {}
708
+ update[:role] = body[:data][:role] || body[:data][:role_name] if body[:data].is_a?(Hash) && (body[:data][:role] || body[:data][:role_name])
709
+ permission = body[:permission] || body[:permissions] || body.dig(:data, :permission) || body.dig(:data, :permissions)
710
+ if permission
711
+ permission = stringify_permission(permission)
712
+ validate_permission_resources!(config, permission)
713
+ unless organization_permission?(ctx, config, require_member!(ctx, session[:user]["id"], organization_id)["role"], permission, organization_id)
714
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE"))
715
+ end
716
+ update[:permission] = JSON.generate(permission)
717
+ end
718
+ update.merge!(additional_input(body[:data], :role, :role_name, :permission, :permissions)) if body[:data].is_a?(Hash)
719
+ updated = ctx.context.adapter.update(model: "organizationRole", where: [{field: "id", value: role["id"]}], update: update)
720
+ ctx.json({success: true, roleData: organization_role_wire(updated)})
721
+ end
722
+ end
723
+
724
+ def organization_delete_role_endpoint(config)
725
+ Endpoint.new(path: "/organization/delete-role", method: "POST") do |ctx|
726
+ session = Routes.current_session(ctx)
727
+ body = normalize_hash(ctx.body)
728
+ organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
729
+ require_org_permission!(ctx, config, session, organization_id, {ac: ["delete"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE"))
730
+ role_name = body[:role] || body[:role_name]
731
+ if role_name && organization_roles(config).key?(role_name.to_s)
732
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("CANNOT_DELETE_A_PRE_DEFINED_ROLE"))
733
+ end
734
+ role = organization_role_by_id(ctx, body[:role_id]) || organization_role_by_name(ctx, organization_id, role_name)
735
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_NOT_FOUND")) unless role
736
+ assigned = ctx.context.adapter.find_many(model: "member", where: [{field: "organizationId", value: organization_id}]).any? do |member|
737
+ member["role"].to_s.split(",").map(&:strip).include?(role["role"])
738
+ end
739
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ROLE_IS_ASSIGNED_TO_MEMBERS")) if assigned
740
+ ctx.context.adapter.delete(model: "organizationRole", where: [{field: "id", value: role["id"]}])
741
+ ctx.json({success: true})
742
+ end
743
+ end
744
+
745
+ def parse_roles(roles)
746
+ Array(roles).join(",")
747
+ end
748
+
749
+ def organization_roles(config)
750
+ (config[:roles] || organization_default_roles(config)).transform_keys(&:to_s)
751
+ end
752
+
753
+ def organization_permission?(ctx, config, role_string, permissions, organization_id)
754
+ roles = organization_roles(config)
755
+ if org_truthy?(config.dig(:dynamic_access_control, :enabled))
756
+ ctx.context.adapter.find_many(model: "organizationRole", where: [{field: "organizationId", value: organization_id}]).each do |entry|
757
+ permission = parse_permission(entry["permission"])
758
+ if roles.key?(entry["role"])
759
+ permission = merge_permissions(roles[entry["role"]].statements, permission)
760
+ end
761
+ roles[entry["role"]] = (config[:ac] || create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)).new_role(permission)
762
+ end
763
+ end
764
+ role_string.to_s.split(",").any? do |role|
765
+ roles[role]&.authorize(permissions || {})&.fetch(:success, false)
766
+ end
767
+ end
768
+
769
+ def require_org_permission!(ctx, config, session, organization_id, permissions, message)
770
+ member = require_member!(ctx, session[:user]["id"], organization_id)
771
+ return member if organization_permission?(ctx, config, member["role"], permissions, organization_id)
772
+
773
+ raise APIError.new("FORBIDDEN", message: message)
774
+ end
775
+
776
+ def merge_permissions(base, extra)
777
+ stringify_permission(base).merge(stringify_permission(extra)) do |_resource, base_actions, extra_actions|
778
+ (Array(base_actions) + Array(extra_actions)).map(&:to_s).uniq
779
+ end
780
+ end
781
+
782
+ def require_member!(ctx, user_id, organization_id)
783
+ member = require_member(ctx, user_id, organization_id)
784
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION")) unless member
785
+
786
+ member
787
+ end
788
+
789
+ def require_member(ctx, user_id, organization_id)
790
+ return nil if user_id.to_s.empty? || organization_id.to_s.empty?
791
+
792
+ ctx.context.adapter.find_one(model: "member", where: [{field: "userId", value: user_id}, {field: "organizationId", value: organization_id}])
793
+ end
794
+
795
+ def require_team_member!(ctx, user_id, team_id)
796
+ member = ctx.context.adapter.find_one(model: "teamMember", where: [{field: "userId", value: user_id}, {field: "teamId", value: team_id}])
797
+ raise APIError.new("FORBIDDEN", message: ORGANIZATION_ERROR_CODES.fetch("USER_IS_NOT_A_MEMBER_OF_THE_TEAM")) unless member
798
+
799
+ member
800
+ end
801
+
802
+ def member_by_id(ctx, id)
803
+ return nil if id.to_s.empty?
804
+
805
+ ctx.context.adapter.find_one(model: "member", where: [{field: "id", value: id}])
806
+ end
807
+
808
+ def find_member_by_email(ctx, organization_id, email)
809
+ user = ctx.context.adapter.find_one(model: "user", where: [{field: "email", value: email.to_s.downcase}])
810
+ user && require_member(ctx, user["id"], organization_id)
811
+ end
812
+
813
+ def organization_by_id(ctx, id)
814
+ return nil if id.to_s.empty?
815
+
816
+ ctx.context.adapter.find_one(model: "organization", where: [{field: "id", value: id}])
817
+ end
818
+
819
+ def organization_by_slug(ctx, slug)
820
+ return nil if slug.to_s.empty?
821
+
822
+ ctx.context.adapter.find_one(model: "organization", where: [{field: "slug", value: slug}])
823
+ end
824
+
825
+ def invitation_by_id(ctx, id)
826
+ return nil if id.to_s.empty?
827
+
828
+ ctx.context.adapter.find_one(model: "invitation", where: [{field: "id", value: id}])
829
+ end
830
+
831
+ def team_by_id(ctx, id)
832
+ return nil if id.to_s.empty?
833
+
834
+ ctx.context.adapter.find_one(model: "team", where: [{field: "id", value: id}])
835
+ end
836
+
837
+ def organization_role_by_id(ctx, id)
838
+ return nil if id.to_s.empty?
839
+
840
+ ctx.context.adapter.find_one(model: "organizationRole", where: [{field: "id", value: id}])
841
+ end
842
+
843
+ def organization_role_by_name(ctx, organization_id, role)
844
+ return nil if role.to_s.empty?
845
+
846
+ ctx.context.adapter.find_one(model: "organizationRole", where: [{field: "organizationId", value: organization_id}, {field: "role", value: role}])
847
+ end
848
+
849
+ def list_members_for(ctx, organization_id, query = {})
850
+ where = [{field: "organizationId", value: organization_id}]
851
+ if query[:filter_field]
852
+ where << {field: query[:filter_field], value: query[:filter_value], operator: query[:filter_operator]}
853
+ elsif query[:filter].is_a?(Hash)
854
+ filter = normalize_hash(query[:filter])
855
+ where << {field: filter[:field], value: filter[:value], operator: filter[:operator]}
856
+ end
857
+ members = ctx.context.adapter.find_many(
858
+ model: "member",
859
+ where: where,
860
+ limit: query[:limit],
861
+ offset: query[:offset],
862
+ sort_by: query[:sort_by] ? {field: query[:sort_by], direction: query[:sort_order] || "asc"} : nil
863
+ )
864
+ {
865
+ members: members.map { |entry| member_wire(ctx, entry) },
866
+ total: ctx.context.adapter.count(model: "member", where: where)
867
+ }
868
+ end
869
+
870
+ def member_wire(ctx, member)
871
+ data = Schema.parse_output(ctx.context.options, "member", member)
872
+ user = ctx.context.internal_adapter.find_user_by_id(member["userId"])
873
+ data["user"] = user.slice("id", "name", "email", "image") if user
874
+ data
875
+ end
876
+
877
+ def organization_wire(ctx, organization)
878
+ data = Schema.parse_output(ctx.context.options, "organization", organization)
879
+ data["metadata"] = parse_metadata(data["metadata"]) if data&.key?("metadata")
880
+ data
881
+ end
882
+
883
+ def invitation_wire(ctx, invitation)
884
+ Schema.parse_output(ctx.context.options, "invitation", invitation)
885
+ end
886
+
887
+ def team_wire(ctx, team)
888
+ Schema.parse_output(ctx.context.options, "team", team)
889
+ end
890
+
891
+ def team_member_wire(ctx, member)
892
+ Schema.parse_output(ctx.context.options, "teamMember", member)
893
+ end
894
+
895
+ def organization_role_wire(role)
896
+ role.merge("permission" => parse_permission(role["permission"]))
897
+ end
898
+
899
+ def ensure_not_last_owner!(ctx, member)
900
+ return unless member["role"].to_s.split(",").include?("owner")
901
+
902
+ owners = ctx.context.adapter.find_many(model: "member", where: [{field: "organizationId", value: member["organizationId"]}]).select { |entry| entry["role"].to_s.split(",").include?("owner") }
903
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER")) if owners.length <= 1
904
+ end
905
+
906
+ def create_default_team(ctx, config, organization, session)
907
+ custom = config.dig(:teams, :default_team, :custom_create_default_team)
908
+ team_data = {organizationId: organization["id"], name: organization["name"], createdAt: Time.now}
909
+ merge_hook_data!(team_data, run_org_hook(config, :before_create_team, {team: team_data, user: session[:user], organization: organization_wire(ctx, organization)}, ctx))
910
+ team = if custom.respond_to?(:call)
911
+ custom.call(organization_wire(ctx, organization), ctx)
912
+ else
913
+ ctx.context.adapter.create(model: "team", data: team_data)
914
+ end
915
+ ctx.context.adapter.create(model: "teamMember", data: {teamId: team["id"], userId: session[:user]["id"], createdAt: Time.now})
916
+ run_org_hook(config, :after_create_team, {team: team_wire(ctx, team), user: session[:user], organization: organization_wire(ctx, organization)}, ctx)
917
+ team
918
+ end
919
+
920
+ def organization_created_count(ctx, user_id)
921
+ members = ctx.context.adapter.find_many(model: "member", where: [{field: "userId", value: user_id}])
922
+ members.count { |member| member["role"].to_s.split(",").include?("owner") }
923
+ end
924
+
925
+ def run_org_hook(config, key, data, ctx)
926
+ hooks = [config.dig(:organization_hooks, key), config.dig(:hooks, key)]
927
+ 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
928
+ 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) }
929
+ end
930
+
931
+ def merge_hook_data!(target, response)
932
+ data = if response.is_a?(Hash)
933
+ normalize_hash(response)[:data]
934
+ end
935
+ target.merge!(normalize_hash(data)) if data.is_a?(Hash)
936
+ target
937
+ end
938
+
939
+ def parse_metadata(value)
940
+ return value if value.nil? || value.is_a?(Hash)
941
+
942
+ JSON.parse(value)
943
+ rescue JSON::ParserError
944
+ value
945
+ end
946
+
947
+ def serialize_metadata(value)
948
+ value.is_a?(Hash) ? JSON.generate(value) : value
949
+ end
950
+
951
+ def parse_permission(value)
952
+ return value if value.is_a?(Hash)
953
+ return {} if value.nil? || value.to_s.empty?
954
+
955
+ JSON.parse(value)
956
+ rescue JSON::ParserError
957
+ {}
958
+ end
959
+
960
+ def stringify_permission(value)
961
+ normalize_hash(value || {}).each_with_object({}) do |(resource, actions), result|
962
+ result[resource.to_s] = Array(actions).map(&:to_s)
963
+ end
964
+ end
965
+
966
+ def validate_permission_resources!(config, permission)
967
+ valid = (config[:ac] || create_access_control(ORGANIZATION_DEFAULT_STATEMENTS)).statements.keys
968
+ invalid = permission.keys - valid
969
+ raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVALID_RESOURCE")) if invalid.any?
970
+ end
971
+
972
+ def organization_team_ids(value)
973
+ Array(value).flat_map { |entry| entry.to_s.split(",") }.map(&:strip).reject(&:empty?)
974
+ end
975
+
976
+ def additional_input(hash, *exclude)
977
+ data = normalize_hash(hash)
978
+ additional = normalize_hash(data.delete(:additional_fields))
979
+ extra_input(data, *exclude, :additional_fields).merge(additional)
980
+ end
981
+
982
+ def extra_input(hash, *exclude)
983
+ normalize_hash(hash).except(*exclude.map(&:to_sym))
984
+ end
985
+
986
+ def org_truthy?(value)
987
+ value == true || value.to_s == "true"
988
+ end
989
+ end
990
+ end