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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +110 -18
- data/lib/better_auth/adapters/base.rb +49 -0
- data/lib/better_auth/adapters/internal_adapter.rb +589 -0
- data/lib/better_auth/adapters/memory.rb +235 -0
- data/lib/better_auth/adapters/mongodb.rb +9 -0
- data/lib/better_auth/adapters/mssql.rb +42 -0
- data/lib/better_auth/adapters/mysql.rb +33 -0
- data/lib/better_auth/adapters/postgres.rb +17 -0
- data/lib/better_auth/adapters/sql.rb +441 -0
- data/lib/better_auth/adapters/sqlite.rb +20 -0
- data/lib/better_auth/api.rb +226 -0
- data/lib/better_auth/api_error.rb +53 -0
- data/lib/better_auth/auth.rb +42 -0
- data/lib/better_auth/configuration.rb +399 -0
- data/lib/better_auth/context.rb +211 -0
- data/lib/better_auth/cookies.rb +278 -0
- data/lib/better_auth/core.rb +37 -1
- data/lib/better_auth/crypto/jwe.rb +76 -0
- data/lib/better_auth/crypto.rb +191 -0
- data/lib/better_auth/database_hooks.rb +114 -0
- data/lib/better_auth/endpoint.rb +326 -0
- data/lib/better_auth/error.rb +52 -0
- data/lib/better_auth/middleware/origin_check.rb +128 -0
- data/lib/better_auth/password.rb +120 -0
- data/lib/better_auth/plugin.rb +142 -0
- data/lib/better_auth/plugin_context.rb +16 -0
- data/lib/better_auth/plugin_registry.rb +67 -0
- data/lib/better_auth/plugins/access.rb +87 -0
- data/lib/better_auth/plugins/additional_fields.rb +29 -0
- data/lib/better_auth/plugins/admin/schema.rb +28 -0
- data/lib/better_auth/plugins/admin.rb +518 -0
- data/lib/better_auth/plugins/anonymous.rb +198 -0
- data/lib/better_auth/plugins/api_key.rb +16 -0
- data/lib/better_auth/plugins/bearer.rb +128 -0
- data/lib/better_auth/plugins/captcha.rb +159 -0
- data/lib/better_auth/plugins/custom_session.rb +84 -0
- data/lib/better_auth/plugins/device_authorization.rb +302 -0
- data/lib/better_auth/plugins/email_otp.rb +536 -0
- data/lib/better_auth/plugins/expo.rb +88 -0
- data/lib/better_auth/plugins/generic_oauth.rb +780 -0
- data/lib/better_auth/plugins/have_i_been_pwned.rb +94 -0
- data/lib/better_auth/plugins/jwt.rb +482 -0
- data/lib/better_auth/plugins/last_login_method.rb +92 -0
- data/lib/better_auth/plugins/magic_link.rb +181 -0
- data/lib/better_auth/plugins/mcp.rb +342 -0
- data/lib/better_auth/plugins/multi_session.rb +173 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +694 -0
- data/lib/better_auth/plugins/oauth_provider.rb +16 -0
- data/lib/better_auth/plugins/oauth_proxy.rb +257 -0
- data/lib/better_auth/plugins/oidc_provider.rb +597 -0
- data/lib/better_auth/plugins/one_tap.rb +154 -0
- data/lib/better_auth/plugins/one_time_token.rb +106 -0
- data/lib/better_auth/plugins/open_api.rb +489 -0
- data/lib/better_auth/plugins/organization/schema.rb +106 -0
- data/lib/better_auth/plugins/organization.rb +995 -0
- data/lib/better_auth/plugins/passkey.rb +16 -0
- data/lib/better_auth/plugins/phone_number.rb +321 -0
- data/lib/better_auth/plugins/scim.rb +16 -0
- data/lib/better_auth/plugins/siwe.rb +242 -0
- data/lib/better_auth/plugins/sso.rb +16 -0
- data/lib/better_auth/plugins/stripe.rb +16 -0
- data/lib/better_auth/plugins/two_factor.rb +514 -0
- data/lib/better_auth/plugins/username.rb +278 -0
- data/lib/better_auth/plugins.rb +46 -0
- data/lib/better_auth/rate_limiter.rb +232 -0
- data/lib/better_auth/request_ip.rb +70 -0
- data/lib/better_auth/router.rb +378 -0
- data/lib/better_auth/routes/account.rb +211 -0
- data/lib/better_auth/routes/email_verification.rb +111 -0
- data/lib/better_auth/routes/error.rb +102 -0
- data/lib/better_auth/routes/ok.rb +15 -0
- data/lib/better_auth/routes/password.rb +183 -0
- data/lib/better_auth/routes/session.rb +160 -0
- data/lib/better_auth/routes/sign_in.rb +90 -0
- data/lib/better_auth/routes/sign_out.rb +15 -0
- data/lib/better_auth/routes/sign_up.rb +196 -0
- data/lib/better_auth/routes/social.rb +367 -0
- data/lib/better_auth/routes/user.rb +205 -0
- data/lib/better_auth/schema/sql.rb +202 -0
- data/lib/better_auth/schema.rb +291 -0
- data/lib/better_auth/session.rb +122 -0
- data/lib/better_auth/session_store.rb +91 -0
- data/lib/better_auth/social_providers/apple.rb +91 -0
- data/lib/better_auth/social_providers/atlassian.rb +32 -0
- data/lib/better_auth/social_providers/base.rb +325 -0
- data/lib/better_auth/social_providers/cognito.rb +32 -0
- data/lib/better_auth/social_providers/discord.rb +81 -0
- data/lib/better_auth/social_providers/dropbox.rb +33 -0
- data/lib/better_auth/social_providers/facebook.rb +35 -0
- data/lib/better_auth/social_providers/figma.rb +31 -0
- data/lib/better_auth/social_providers/github.rb +74 -0
- data/lib/better_auth/social_providers/gitlab.rb +67 -0
- data/lib/better_auth/social_providers/google.rb +90 -0
- data/lib/better_auth/social_providers/huggingface.rb +31 -0
- data/lib/better_auth/social_providers/kakao.rb +32 -0
- data/lib/better_auth/social_providers/kick.rb +32 -0
- data/lib/better_auth/social_providers/line.rb +33 -0
- data/lib/better_auth/social_providers/linear.rb +44 -0
- data/lib/better_auth/social_providers/linkedin.rb +30 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +137 -0
- data/lib/better_auth/social_providers/naver.rb +31 -0
- data/lib/better_auth/social_providers/notion.rb +33 -0
- data/lib/better_auth/social_providers/paybin.rb +31 -0
- data/lib/better_auth/social_providers/paypal.rb +36 -0
- data/lib/better_auth/social_providers/polar.rb +31 -0
- data/lib/better_auth/social_providers/railway.rb +49 -0
- data/lib/better_auth/social_providers/reddit.rb +32 -0
- data/lib/better_auth/social_providers/roblox.rb +31 -0
- data/lib/better_auth/social_providers/salesforce.rb +38 -0
- data/lib/better_auth/social_providers/slack.rb +30 -0
- data/lib/better_auth/social_providers/spotify.rb +31 -0
- data/lib/better_auth/social_providers/tiktok.rb +35 -0
- data/lib/better_auth/social_providers/twitch.rb +39 -0
- data/lib/better_auth/social_providers/twitter.rb +32 -0
- data/lib/better_auth/social_providers/vercel.rb +47 -0
- data/lib/better_auth/social_providers/vk.rb +34 -0
- data/lib/better_auth/social_providers/wechat.rb +104 -0
- data/lib/better_auth/social_providers/zoom.rb +31 -0
- data/lib/better_auth/social_providers.rb +38 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +86 -2
- metadata +233 -21
- data/.ruby-version +0 -1
- data/.standard.yml +0 -12
- data/.vscode/settings.json +0 -22
- data/AGENTS.md +0 -50
- data/CLAUDE.md +0 -1
- data/CODE_OF_CONDUCT.md +0 -173
- data/CONTRIBUTING.md +0 -187
- data/Gemfile +0 -12
- data/Makefile +0 -207
- data/Rakefile +0 -25
- data/SECURITY.md +0 -28
- 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
|