organizations 0.6.0 → 0.7.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/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +7 -0
- data/.simplecov +17 -10
- data/CHANGELOG.md +93 -0
- data/README.md +687 -33
- data/app/controllers/organizations/invitations_controller.rb +27 -22
- data/app/controllers/organizations/memberships_controller.rb +37 -30
- data/app/controllers/organizations/organizations_controller.rb +8 -8
- data/app/controllers/organizations/public_invitations_controller.rb +1 -1
- data/app/mailers/organizations/invitation_mailer.rb +2 -2
- data/app/mailers/organizations/verification_mailer.rb +1 -1
- data/app/models/organizations/grant.rb +6 -0
- data/app/views/organizations/invitation_mailer/invitation_email.html.erb +11 -11
- data/app/views/organizations/invitation_mailer/invitation_email.text.erb +7 -7
- data/app/views/organizations/verification_mailer/code_email.html.erb +3 -3
- data/app/views/organizations/verification_mailer/code_email.text.erb +3 -3
- data/config/locales/en.yml +22 -0
- data/config/locales/es.yml +22 -0
- data/config/routes.rb +1 -2
- data/gemfiles/rails_7.2.gemfile +5 -1
- data/gemfiles/rails_8.1.gemfile +5 -1
- data/lib/generators/organizations/grants/grants_generator.rb +47 -0
- data/lib/generators/organizations/grants/templates/create_organizations_grants.rb.erb +59 -0
- data/lib/generators/organizations/install/install_generator.rb +1 -0
- data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +15 -0
- data/lib/generators/organizations/join/join_generator.rb +113 -0
- data/lib/generators/organizations/join/templates/app/controllers/organization_access_controller.rb +146 -0
- data/lib/generators/organizations/join/templates/app/controllers/organization_joins_controller.rb +93 -0
- data/lib/generators/organizations/join/templates/app/views/organization_access/show.html.erb +142 -0
- data/lib/generators/organizations/join/templates/app/views/organization_joins/_disclosure.html.erb +8 -0
- data/lib/generators/organizations/join/templates/app/views/organization_joins/show.html.erb +113 -0
- data/lib/generators/organizations/join/templates/routes.rb +19 -0
- data/lib/generators/organizations/upgrade/templates/add_domain_verification.rb.erb +29 -0
- data/lib/generators/organizations/upgrade/templates/add_personal_organizations.rb.erb +22 -0
- data/lib/generators/organizations/upgrade/upgrade_generator.rb +79 -14
- data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +1 -1
- data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +2 -2
- data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +1 -1
- data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +21 -13
- data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +1 -1
- data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +3 -3
- data/lib/generators/organizations/views/views_generator.rb +4 -6
- data/lib/organizations/account_deletion_plan.rb +118 -0
- data/lib/organizations/configuration.rb +152 -0
- data/lib/organizations/controller_helpers.rb +55 -15
- data/lib/organizations/grant_access.rb +75 -0
- data/lib/organizations/grant_holder.rb +152 -0
- data/lib/organizations/grantable.rb +28 -0
- data/lib/organizations/join_flow.rb +16 -12
- data/lib/organizations/models/concerns/has_organizations.rb +217 -9
- data/lib/organizations/models/domain.rb +283 -1
- data/lib/organizations/models/grant.rb +152 -0
- data/lib/organizations/models/invitation.rb +76 -7
- data/lib/organizations/models/join_request.rb +102 -43
- data/lib/organizations/models/membership.rb +87 -5
- data/lib/organizations/models/organization.rb +219 -48
- data/lib/organizations/organization_scoped.rb +16 -0
- data/lib/organizations/personal_owner_backfill.rb +122 -0
- data/lib/organizations/public_email_domains.rb +49 -0
- data/lib/organizations/resource.rb +11 -0
- data/lib/organizations/roles.rb +55 -0
- data/lib/organizations/version.rb +1 -1
- data/lib/organizations/view_helpers.rb +40 -9
- data/lib/organizations.rb +99 -4
- metadata +22 -2
|
@@ -35,55 +35,60 @@ module Organizations
|
|
|
35
35
|
email = invitation_params[:email]
|
|
36
36
|
role = invitation_params[:role] || ::Organizations.configuration.default_invitation_role
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
38
|
+
@invitation = current_user.send_organization_invite_to!(
|
|
39
|
+
email,
|
|
40
|
+
organization: current_organization,
|
|
41
|
+
role: role
|
|
42
|
+
)
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
respond_to do |format|
|
|
45
|
+
return_to = params[:return_to].presence || organization_invitations_path
|
|
46
|
+
format.html { redirect_to return_to, notice: "Invitation sent to #{email}" }
|
|
47
|
+
format.json { render json: invitation_json(@invitation), status: :created }
|
|
48
|
+
end
|
|
49
|
+
rescue ::Organizations::NotAuthorized => e # e.g. a role the inviter may not act on
|
|
50
|
+
handle_not_authorized(e)
|
|
51
|
+
rescue ::Organizations::InvitationError, ::Organizations::Organization::CannotInviteAsOwner,
|
|
52
|
+
ActiveRecord::RecordInvalid, ArgumentError => e
|
|
53
|
+
respond_to do |format|
|
|
54
|
+
format.html do
|
|
55
|
+
@invitation = current_organization.invitations.build(invitation_params)
|
|
56
|
+
flash.now[:alert] = e.message
|
|
57
|
+
render :new, status: 422
|
|
58
58
|
end
|
|
59
|
+
format.json { render json: { error: e.message }, status: 422 }
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
# DELETE /invitations/:id
|
|
63
64
|
# Revoke/delete an invitation
|
|
64
65
|
def destroy
|
|
65
|
-
@invitation.
|
|
66
|
+
@invitation.revoke!(revoked_by: current_user)
|
|
66
67
|
|
|
67
68
|
respond_to do |format|
|
|
68
69
|
format.html { redirect_back fallback_location: organization_invitations_path, notice: "Invitation revoked" }
|
|
69
70
|
format.json { head :no_content }
|
|
70
71
|
end
|
|
72
|
+
rescue ::Organizations::NotAuthorized => e # an invitation for a role the requester may not act on
|
|
73
|
+
handle_not_authorized(e)
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
# POST /invitations/:id/resend
|
|
74
77
|
# Resend an invitation email
|
|
75
78
|
def resend
|
|
76
79
|
begin
|
|
77
|
-
@invitation.resend!
|
|
80
|
+
@invitation.resend!(resent_by: current_user)
|
|
78
81
|
|
|
79
82
|
respond_to do |format|
|
|
80
83
|
format.html { redirect_back fallback_location: organization_invitations_path, notice: "Invitation resent to #{@invitation.email}" }
|
|
81
84
|
format.json { render json: invitation_json(@invitation) }
|
|
82
85
|
end
|
|
86
|
+
rescue ::Organizations::NotAuthorized => e # an invitation for a role the requester may not act on
|
|
87
|
+
handle_not_authorized(e)
|
|
83
88
|
rescue ::Organizations::InvitationAlreadyAccepted => e
|
|
84
89
|
respond_to do |format|
|
|
85
90
|
format.html { redirect_back fallback_location: organization_invitations_path, alert: e.message }
|
|
86
|
-
format.json { render json: { error: e.message }, status:
|
|
91
|
+
format.json { render json: { error: e.message }, status: 422 }
|
|
87
92
|
end
|
|
88
93
|
end
|
|
89
94
|
end
|
|
@@ -28,36 +28,35 @@ module Organizations
|
|
|
28
28
|
def update
|
|
29
29
|
# Validate the requester can make this change
|
|
30
30
|
validate_role_change!
|
|
31
|
-
|
|
32
31
|
new_role = membership_params[:role].to_sym
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
33
|
+
# Use the invariant-safe domain method that enforces owner rules
|
|
34
|
+
current_organization.change_role_of!(
|
|
35
|
+
@membership.user,
|
|
36
|
+
to: new_role,
|
|
37
|
+
changed_by: current_user
|
|
38
|
+
)
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
# Reload to get updated role
|
|
41
|
+
@membership.reload
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
respond_to do |format|
|
|
44
|
+
format.html { redirect_back fallback_location: memberships_path, notice: "Role updated successfully." }
|
|
45
|
+
format.json { render json: membership_json(@membership) }
|
|
46
|
+
end
|
|
47
|
+
rescue Organizations::NotAuthorized => e # own role, or a role above the requester's
|
|
48
|
+
handle_not_authorized(e)
|
|
49
|
+
rescue Organizations::Organization::CannotHaveMultipleOwners,
|
|
50
|
+
Organizations::Organization::CannotDemoteOwner,
|
|
51
|
+
Organizations::Error => e
|
|
52
|
+
respond_to do |format|
|
|
53
|
+
format.html { redirect_back fallback_location: memberships_path, alert: e.message }
|
|
54
|
+
format.json { render json: { error: e.message }, status: 422 }
|
|
55
|
+
end
|
|
56
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
57
|
+
respond_to do |format|
|
|
58
|
+
format.html { redirect_back fallback_location: memberships_path, alert: e.record.errors.full_messages.join(", ") }
|
|
59
|
+
format.json { render json: { errors: e.record.errors }, status: 422 }
|
|
61
60
|
end
|
|
62
61
|
end
|
|
63
62
|
|
|
@@ -72,10 +71,12 @@ module Organizations
|
|
|
72
71
|
format.html { redirect_back fallback_location: memberships_path, notice: "Member removed successfully." }
|
|
73
72
|
format.json { head :no_content }
|
|
74
73
|
end
|
|
74
|
+
rescue Organizations::NotAuthorized => e # self, the owner, or a role above the requester's
|
|
75
|
+
handle_not_authorized(e)
|
|
75
76
|
rescue Organizations::Organization::CannotRemoveOwner, Organizations::Error => e
|
|
76
77
|
respond_to do |format|
|
|
77
78
|
format.html { redirect_back fallback_location: memberships_path, alert: e.message }
|
|
78
|
-
format.json { render json: { error: e.message }, status:
|
|
79
|
+
format.json { render json: { error: e.message }, status: 422 }
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
|
|
@@ -99,7 +100,7 @@ module Organizations
|
|
|
99
100
|
Error => e
|
|
100
101
|
respond_to do |format|
|
|
101
102
|
format.html { redirect_back fallback_location: organizations_path, alert: e.message }
|
|
102
|
-
format.json { render json: { error: e.message }, status:
|
|
103
|
+
format.json { render json: { error: e.message }, status: 422 }
|
|
103
104
|
end
|
|
104
105
|
end
|
|
105
106
|
end
|
|
@@ -121,7 +122,7 @@ module Organizations
|
|
|
121
122
|
Organizations::Error => e
|
|
122
123
|
respond_to do |format|
|
|
123
124
|
format.html { redirect_back fallback_location: memberships_path, alert: e.message }
|
|
124
|
-
format.json { render json: { error: e.message }, status:
|
|
125
|
+
format.json { render json: { error: e.message }, status: 422 }
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
end
|
|
@@ -155,7 +156,9 @@ module Organizations
|
|
|
155
156
|
end
|
|
156
157
|
|
|
157
158
|
# Note: Owner promotion/demotion rules are enforced by Organization#change_role_of!
|
|
158
|
-
# which will raise CannotHaveMultipleOwners or CannotDemoteOwner as appropriate
|
|
159
|
+
# which will raise CannotHaveMultipleOwners or CannotDemoteOwner as appropriate.
|
|
160
|
+
# It also raises NotAuthorized when the requester may not act on the member's
|
|
161
|
+
# current role or the new role; #update answers that like a failed permission guard.
|
|
159
162
|
end
|
|
160
163
|
|
|
161
164
|
def validate_removal!
|
|
@@ -178,6 +181,10 @@ module Organizations
|
|
|
178
181
|
user: current_user
|
|
179
182
|
)
|
|
180
183
|
end
|
|
184
|
+
|
|
185
|
+
# Note: Organization#remove_member! raises NotAuthorized when the requester
|
|
186
|
+
# may not act on the member's role; #destroy answers that like a failed
|
|
187
|
+
# permission guard.
|
|
181
188
|
end
|
|
182
189
|
|
|
183
190
|
# JSON serialization helpers
|
|
@@ -51,18 +51,18 @@ module Organizations
|
|
|
51
51
|
format.html do
|
|
52
52
|
@organization = Organization.new(organization_params)
|
|
53
53
|
flash.now[:alert] = e.message
|
|
54
|
-
render :new, status:
|
|
54
|
+
render :new, status: 422
|
|
55
55
|
end
|
|
56
|
-
format.json { render json: { error: e.message }, status:
|
|
56
|
+
format.json { render json: { error: e.message }, status: 422 }
|
|
57
57
|
end
|
|
58
58
|
rescue ActiveRecord::RecordInvalid => e
|
|
59
59
|
respond_to do |format|
|
|
60
60
|
format.html do
|
|
61
61
|
@organization = Organization.new(organization_params)
|
|
62
62
|
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
|
63
|
-
render :new, status:
|
|
63
|
+
render :new, status: 422
|
|
64
64
|
end
|
|
65
|
-
format.json { render json: { errors: e.record.errors }, status:
|
|
65
|
+
format.json { render json: { errors: e.record.errors }, status: 422 }
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -82,8 +82,8 @@ module Organizations
|
|
|
82
82
|
end
|
|
83
83
|
else
|
|
84
84
|
respond_to do |format|
|
|
85
|
-
format.html { render :edit, status:
|
|
86
|
-
format.json { render json: { errors: @organization.errors }, status:
|
|
85
|
+
format.html { render :edit, status: 422 }
|
|
86
|
+
format.json { render json: { errors: @organization.errors }, status: 422 }
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
end
|
|
@@ -132,14 +132,14 @@ module Organizations
|
|
|
132
132
|
role = current_user.role_in(@organization)
|
|
133
133
|
return if role && Roles.has_permission?(role, :manage_settings)
|
|
134
134
|
|
|
135
|
-
handle_unauthorized(permission: :manage_settings)
|
|
135
|
+
handle_unauthorized(permission: :manage_settings, organization: @organization)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def authorize_delete_organization!
|
|
139
139
|
role = current_user.role_in(@organization)
|
|
140
140
|
return if role && Roles.has_permission?(role, :delete_organization)
|
|
141
141
|
|
|
142
|
-
handle_unauthorized(permission: :delete_organization)
|
|
142
|
+
handle_unauthorized(permission: :delete_organization, organization: @organization)
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
# JSON serialization helpers
|
|
@@ -36,14 +36,14 @@ module Organizations
|
|
|
36
36
|
mail(
|
|
37
37
|
to: invitation.email,
|
|
38
38
|
subject: Organizations.t(:"mailers.invitation.subject",
|
|
39
|
-
inviter: @inviter_name, organization: @organization
|
|
39
|
+
inviter: @inviter_name, organization: @organization)
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
45
|
def inviter_name
|
|
46
|
-
return Organizations.t(:"mailers.from_team") unless @inviter
|
|
46
|
+
return Organizations.t(:"mailers.from_team", organization: @organization) unless @inviter
|
|
47
47
|
|
|
48
48
|
if @inviter.respond_to?(:name) && @inviter.name.present?
|
|
49
49
|
@inviter.name
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reloadable entrypoint for Organizations::Grant in Rails apps.
|
|
4
|
+
# This file lives in app/models so Zeitwerk manages it, making the class
|
|
5
|
+
# reload-safe. It delegates to the canonical implementation in lib/.
|
|
6
|
+
load File.expand_path("../../../lib/organizations/models/grant.rb", __dir__)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title><%= Organizations.t(:"mailers.invitation.title", organization: @organization
|
|
6
|
+
<title><%= Organizations.t(:"mailers.invitation.title", organization: @organization) %></title>
|
|
7
7
|
<style>
|
|
8
8
|
body {
|
|
9
9
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div class="header">
|
|
56
|
-
<h1><%= Organizations.t(:"mailers.invitation.header") %></h1>
|
|
56
|
+
<h1><%= Organizations.t(:"mailers.invitation.header", organization: @organization) %></h1>
|
|
57
57
|
</div>
|
|
58
58
|
|
|
59
59
|
<div class="content">
|
|
60
|
-
<p><%= Organizations.t(:"mailers.invitation.greeting") %></p>
|
|
60
|
+
<p><%= Organizations.t(:"mailers.invitation.greeting", organization: @organization) %></p>
|
|
61
61
|
|
|
62
62
|
<p>
|
|
63
63
|
<%# The *_html keys go through the view `t` helper: its _html-suffix
|
|
@@ -65,36 +65,36 @@
|
|
|
65
65
|
interpolated value (inviter/org names are user input). Source:
|
|
66
66
|
https://guides.rubyonrails.org/i18n.html#using-safe-html-translations %>
|
|
67
67
|
<% if @inviter %>
|
|
68
|
-
|
|
68
|
+
<% primary, *fallbacks = Organizations.i18n_keys(:"mailers.invitation.invited_by_html", organization: @organization) %><%= t(primary, default: fallbacks, inviter: @inviter_name, organization: @organization.name) %>
|
|
69
69
|
<% else %>
|
|
70
|
-
|
|
70
|
+
<% primary, *fallbacks = Organizations.i18n_keys(:"mailers.invitation.invited_generic_html", organization: @organization) %><%= t(primary, default: fallbacks, organization: @organization.name) %>
|
|
71
71
|
<% end %>
|
|
72
72
|
</p>
|
|
73
73
|
|
|
74
74
|
<p>
|
|
75
|
-
<%= Organizations.t(:"mailers.invitation.joining_as_label") %>
|
|
76
|
-
<span class="role-badge"><%= Organizations.t(:"roles.#{@invitation.role}", default: @invitation.role.to_s.capitalize) %></span>
|
|
75
|
+
<%= Organizations.t(:"mailers.invitation.joining_as_label", organization: @organization) %>
|
|
76
|
+
<span class="role-badge"><%= Organizations.t(:"roles.#{@invitation.role}", organization: @organization, default: @invitation.role.to_s.capitalize) %></span>
|
|
77
77
|
</p>
|
|
78
78
|
|
|
79
79
|
<p style="text-align: center;">
|
|
80
|
-
<a href="<%= @accept_url %>" class="button"><%= Organizations.t(:"mailers.invitation.accept_button") %></a>
|
|
80
|
+
<a href="<%= @accept_url %>" class="button"><%= Organizations.t(:"mailers.invitation.accept_button", organization: @organization) %></a>
|
|
81
81
|
</p>
|
|
82
82
|
|
|
83
83
|
<p>
|
|
84
|
-
<%= Organizations.t(:"mailers.invitation.copy_link") %><br>
|
|
84
|
+
<%= Organizations.t(:"mailers.invitation.copy_link", organization: @organization) %><br>
|
|
85
85
|
<a href="<%= @accept_url %>"><%= @accept_url %></a>
|
|
86
86
|
</p>
|
|
87
87
|
|
|
88
88
|
<% if @expires_on %>
|
|
89
89
|
<p style="color: #666; font-size: 14px;">
|
|
90
|
-
<%= Organizations.t(:"mailers.invitation.expires_on", date: @expires_on) %>
|
|
90
|
+
<%= Organizations.t(:"mailers.invitation.expires_on", organization: @organization, date: @expires_on) %>
|
|
91
91
|
</p>
|
|
92
92
|
<% end %>
|
|
93
93
|
</div>
|
|
94
94
|
|
|
95
95
|
<div class="footer">
|
|
96
96
|
<p>
|
|
97
|
-
<%= Organizations.t(:"mailers.invitation.ignore") %>
|
|
97
|
+
<%= Organizations.t(:"mailers.invitation.ignore", organization: @organization) %>
|
|
98
98
|
</p>
|
|
99
99
|
</div>
|
|
100
100
|
</body>
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
<%= Organizations.t(:"mailers.invitation.title", organization: @organization
|
|
1
|
+
<%= Organizations.t(:"mailers.invitation.title", organization: @organization) %>
|
|
2
2
|
|
|
3
|
-
<%= Organizations.t(:"mailers.invitation.greeting") %>
|
|
3
|
+
<%= Organizations.t(:"mailers.invitation.greeting", organization: @organization) %>
|
|
4
4
|
|
|
5
|
-
<% if @inviter %><%= Organizations.t(:"mailers.invitation.invited_by",
|
|
5
|
+
<% if @inviter %><%= Organizations.t(:"mailers.invitation.invited_by", organization: @organization, inviter: @inviter_name) %><% else %><%= Organizations.t(:"mailers.invitation.invited_generic", organization: @organization) %><% end %>
|
|
6
6
|
|
|
7
|
-
<%= Organizations.t(:"mailers.invitation.joining_as", role: Organizations.t(:"roles.#{@invitation.role}", default: @invitation.role.to_s.capitalize)) %>
|
|
7
|
+
<%= Organizations.t(:"mailers.invitation.joining_as", organization: @organization, role: Organizations.t(:"roles.#{@invitation.role}", organization: @organization, default: @invitation.role.to_s.capitalize)) %>
|
|
8
8
|
|
|
9
|
-
<%= Organizations.t(:"mailers.invitation.accept_cta") %>
|
|
9
|
+
<%= Organizations.t(:"mailers.invitation.accept_cta", organization: @organization) %>
|
|
10
10
|
<%= @accept_url %>
|
|
11
11
|
|
|
12
12
|
<% if @expires_on %>
|
|
13
|
-
<%= Organizations.t(:"mailers.invitation.expires_on", date: @expires_on) %>
|
|
13
|
+
<%= Organizations.t(:"mailers.invitation.expires_on", organization: @organization, date: @expires_on) %>
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
<%= Organizations.t(:"mailers.invitation.ignore") %>
|
|
18
|
+
<%= Organizations.t(:"mailers.invitation.ignore", organization: @organization) %>
|
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
<%# _html key via the view `t` helper: html_safe translation, escaped
|
|
8
8
|
interpolations (the org name is user input). Source:
|
|
9
9
|
https://guides.rubyonrails.org/i18n.html#using-safe-html-translations %>
|
|
10
|
-
<p
|
|
10
|
+
<p><% primary, *fallbacks = Organizations.i18n_keys(:"mailers.verification.lead_html", organization: @organization) %><%= t(primary, default: fallbacks, organization: @organization.name) %></p>
|
|
11
11
|
|
|
12
12
|
<p style="font-size: 32px; font-weight: 700; letter-spacing: 6px; background: #f3f4f6; border-radius: 8px; display: inline-block; padding: 12px 24px;">
|
|
13
13
|
<%= @code %>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
<p><%= Organizations.t(:"mailers.verification.expires", minutes: @expires_in_minutes) %></p>
|
|
16
|
+
<p><%= Organizations.t(:"mailers.verification.expires", organization: @organization, minutes: @expires_in_minutes) %></p>
|
|
17
17
|
|
|
18
18
|
<p style="color: #6b7280; font-size: 14px;">
|
|
19
|
-
<%= Organizations.t(:"mailers.verification.ignore", organization: @organization
|
|
19
|
+
<%= Organizations.t(:"mailers.verification.ignore", organization: @organization) %>
|
|
20
20
|
</p>
|
|
21
21
|
</body>
|
|
22
22
|
</html>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<%= Organizations.t(:"mailers.verification.lead", organization: @organization
|
|
1
|
+
<%= Organizations.t(:"mailers.verification.lead", organization: @organization) %>
|
|
2
2
|
|
|
3
3
|
<%= @code %>
|
|
4
4
|
|
|
5
|
-
<%= Organizations.t(:"mailers.verification.expires", minutes: @expires_in_minutes) %>
|
|
5
|
+
<%= Organizations.t(:"mailers.verification.expires", organization: @organization, minutes: @expires_in_minutes) %>
|
|
6
6
|
|
|
7
|
-
<%= Organizations.t(:"mailers.verification.ignore", organization: @organization
|
|
7
|
+
<%= Organizations.t(:"mailers.verification.ignore", organization: @organization) %>
|
data/config/locales/en.yml
CHANGED
|
@@ -79,18 +79,34 @@ en:
|
|
|
79
79
|
invite_as_owner: "Cannot invite as owner. Invite as admin, then use transfer_ownership_to! after they join."
|
|
80
80
|
invite_not_a_member: "Only organization members can send invitations"
|
|
81
81
|
invite_not_authorized: "You don't have permission to invite members"
|
|
82
|
+
invite_role_outranks_actor: "You can't invite someone as %{role} because that role ranks above yours"
|
|
83
|
+
invitation_management_outranks_actor: "You can't revoke or resend an invitation for the %{role} role because it ranks above yours"
|
|
82
84
|
|
|
83
85
|
# — Ownership / role invariants (reachable from admin UIs) —
|
|
84
86
|
cannot_remove_owner: "Cannot remove the organization owner. Transfer ownership first."
|
|
87
|
+
member_removal_outranks_actor: "You can't remove a member with the %{role} role because it ranks above yours"
|
|
85
88
|
cannot_add_as_owner: "Cannot add member as owner. Use transfer_ownership_to! instead."
|
|
86
89
|
cannot_promote_to_owner: "Cannot promote to owner. Use transfer_ownership_to! instead."
|
|
87
90
|
cannot_demote_owner_directly: "Cannot demote owner directly. Use transfer_ownership_to! instead."
|
|
88
91
|
cannot_demote_owner: "Cannot demote owner. Transfer ownership first."
|
|
89
92
|
promote_not_higher: "Cannot promote to %{new_role} - it's not a higher role than %{role}"
|
|
90
93
|
demote_not_lower: "Cannot demote to %{new_role} - it's not a lower role than %{role}"
|
|
94
|
+
role_change_outranks_actor: "You can't give or take away the %{role} role because it ranks above yours"
|
|
95
|
+
role_has_permissions_actor_lacks: "You can't act on the %{role} role because it has permissions you do not have"
|
|
91
96
|
transfer_no_owner: "Cannot transfer ownership because organization has no owner membership"
|
|
92
97
|
transfer_to_non_member: "Cannot transfer ownership to a non-member"
|
|
93
98
|
transfer_to_non_admin: "Cannot transfer ownership to non-admin. Promote them to admin first."
|
|
99
|
+
transfer_personal_organization: "A personal organization stays with its owner. Convert it to a shared organization first."
|
|
100
|
+
domain_operator_is_member: "A member of the organization cannot approve its own domain claim."
|
|
101
|
+
grant_not_authorized: "You don't have permission to manage access to resources."
|
|
102
|
+
grant_outranks_actor: "You can't manage the access of a member whose role ranks above yours."
|
|
103
|
+
grant_exceeds_actor: "You can only give access that you have yourself."
|
|
104
|
+
domain_verification:
|
|
105
|
+
not_started: "Start the domain verification first."
|
|
106
|
+
expired: "The domain verification expired. Start it again."
|
|
107
|
+
changed: "The domain claim changed during the check. Start the verification again."
|
|
108
|
+
record_not_found: "The DNS TXT record was not found. DNS changes can take a while to spread."
|
|
109
|
+
lookup_failed: "The DNS lookup failed. Try again later."
|
|
94
110
|
|
|
95
111
|
# — Leaving / user-level —
|
|
96
112
|
cannot_leave_as_last_owner: "Cannot leave organization as the only owner. Transfer ownership first."
|
|
@@ -119,6 +135,12 @@ en:
|
|
|
119
135
|
attributes:
|
|
120
136
|
membership_taken: "is already a member of this organization"
|
|
121
137
|
owner_taken: "owner already exists for this organization"
|
|
138
|
+
personal_owner_not_owner: "must be the owner of the organization"
|
|
139
|
+
domain_blocked: "is a public email provider and cannot be claimed by an organization"
|
|
140
|
+
join_policy_invalid: "must be none, suggest or auto_join"
|
|
141
|
+
grant_one_holder: "must belong to exactly one membership or invitation"
|
|
142
|
+
grant_capabilities_blank: "must list at least one capability"
|
|
143
|
+
grant_capabilities_unknown: "include unknown capabilities: %{capabilities}"
|
|
122
144
|
pending_request_taken: "already has a pending request for this organization"
|
|
123
145
|
invitation_taken: "has already been invited to this organization"
|
|
124
146
|
allowlist_taken: "is already on this organization's allowlist"
|
data/config/locales/es.yml
CHANGED
|
@@ -61,17 +61,33 @@ es:
|
|
|
61
61
|
invite_as_owner: "No se puede invitar como propietario. Invita como administrador y transfiere la propiedad después."
|
|
62
62
|
invite_not_a_member: "Solo los miembros de la organización pueden enviar invitaciones"
|
|
63
63
|
invite_not_authorized: "No tienes permiso para invitar miembros"
|
|
64
|
+
invite_role_outranks_actor: "No puedes invitar a nadie como %{role} porque ese rol está por encima del tuyo"
|
|
65
|
+
invitation_management_outranks_actor: "No puedes revocar ni reenviar una invitación para el rol %{role} porque está por encima del tuyo"
|
|
64
66
|
|
|
65
67
|
cannot_remove_owner: "No se puede eliminar al propietario de la organización. Transfiere la propiedad primero."
|
|
68
|
+
member_removal_outranks_actor: "No puedes eliminar a un miembro con el rol %{role} porque está por encima del tuyo"
|
|
66
69
|
cannot_add_as_owner: "No se puede añadir un miembro como propietario. Usa la transferencia de propiedad."
|
|
67
70
|
cannot_promote_to_owner: "No se puede ascender a propietario. Usa la transferencia de propiedad."
|
|
68
71
|
cannot_demote_owner_directly: "No se puede degradar al propietario directamente. Usa la transferencia de propiedad."
|
|
69
72
|
cannot_demote_owner: "No se puede degradar al propietario. Transfiere la propiedad primero."
|
|
70
73
|
promote_not_higher: "No se puede ascender a %{new_role}: no es un rol superior a %{role}"
|
|
71
74
|
demote_not_lower: "No se puede degradar a %{new_role}: no es un rol inferior a %{role}"
|
|
75
|
+
role_change_outranks_actor: "No puedes asignar ni retirar el rol %{role} porque está por encima del tuyo"
|
|
76
|
+
role_has_permissions_actor_lacks: "No puedes actuar sobre el rol %{role} porque tiene permisos que tú no tienes"
|
|
72
77
|
transfer_no_owner: "No se puede transferir la propiedad porque la organización no tiene propietario"
|
|
73
78
|
transfer_to_non_member: "No se puede transferir la propiedad a alguien que no es miembro"
|
|
74
79
|
transfer_to_non_admin: "No se puede transferir la propiedad a alguien que no es administrador. Asciéndele primero."
|
|
80
|
+
transfer_personal_organization: "Una organización personal pertenece a su propietario. Conviértela primero en una organización compartida."
|
|
81
|
+
domain_operator_is_member: "Un miembro de la organización no puede aprobar su propio dominio."
|
|
82
|
+
grant_not_authorized: "No tienes permiso para gestionar el acceso a los recursos."
|
|
83
|
+
grant_outranks_actor: "No puedes gestionar el acceso de un miembro cuyo rol es superior al tuyo."
|
|
84
|
+
grant_exceeds_actor: "Solo puedes dar el acceso que tú tienes."
|
|
85
|
+
domain_verification:
|
|
86
|
+
not_started: "Inicia primero la verificación del dominio."
|
|
87
|
+
expired: "La verificación del dominio ha caducado. Iníciala de nuevo."
|
|
88
|
+
changed: "El dominio cambió durante la comprobación. Inicia la verificación de nuevo."
|
|
89
|
+
record_not_found: "No se encontró el registro DNS TXT. Los cambios de DNS pueden tardar en propagarse."
|
|
90
|
+
lookup_failed: "La consulta DNS ha fallado. Inténtalo más tarde."
|
|
75
91
|
|
|
76
92
|
cannot_leave_as_last_owner: "No puedes salir de la organización siendo su único propietario. Transfiere la propiedad primero."
|
|
77
93
|
cannot_leave_last_organization: "No puedes salir de tu única organización"
|
|
@@ -95,6 +111,12 @@ es:
|
|
|
95
111
|
attributes:
|
|
96
112
|
membership_taken: "ya es miembro de esta organización"
|
|
97
113
|
owner_taken: "ya existe un propietario para esta organización"
|
|
114
|
+
personal_owner_not_owner: "debe ser el propietario de la organización"
|
|
115
|
+
domain_blocked: "es un proveedor de correo público y ninguna organización puede reclamarlo"
|
|
116
|
+
join_policy_invalid: "debe ser none, suggest o auto_join"
|
|
117
|
+
grant_one_holder: "debe pertenecer a una sola membresía o invitación"
|
|
118
|
+
grant_capabilities_blank: "debe indicar al menos una capacidad"
|
|
119
|
+
grant_capabilities_unknown: "incluyen capacidades desconocidas: %{capabilities}"
|
|
98
120
|
pending_request_taken: "ya tiene una solicitud pendiente para esta organización"
|
|
99
121
|
invitation_taken: "ya ha sido invitado a esta organización"
|
|
100
122
|
allowlist_taken: "ya está en la lista de esta organización"
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Metrics/BlockLength -- a routes file is one declarative block by nature
|
|
3
|
+
# rubocop:disable-next Metrics/BlockLength -- a routes file is one declarative block by nature
|
|
4
4
|
Organizations::Engine.routes.draw do
|
|
5
5
|
# Route groups are drawn per config.engine_routes (devise_for-style
|
|
6
6
|
# only:/except:) so hosts declare what they want instead of shadowing
|
|
@@ -55,4 +55,3 @@ Organizations::Engine.routes.draw do
|
|
|
55
55
|
post "invitations/:token/accept", to: "public_invitations#accept", as: :accept_invitation
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
|
-
# rubocop:enable Metrics/BlockLength
|
data/gemfiles/rails_7.2.gemfile
CHANGED
|
@@ -8,7 +8,7 @@ gem "json", "< 3"
|
|
|
8
8
|
|
|
9
9
|
group :development do
|
|
10
10
|
gem "irb"
|
|
11
|
-
gem "rubocop", "~> 1.
|
|
11
|
+
gem "rubocop", "~> 1.92"
|
|
12
12
|
gem "rubocop-minitest", "~> 0.39"
|
|
13
13
|
gem "rubocop-performance", "~> 1.26"
|
|
14
14
|
end
|
|
@@ -19,8 +19,12 @@ group :development, :test do
|
|
|
19
19
|
gem "minitest-mock"
|
|
20
20
|
gem "rack-test"
|
|
21
21
|
gem "sqlite3", ">= 2.1"
|
|
22
|
+
gem "pg"
|
|
22
23
|
gem "ostruct"
|
|
23
24
|
gem "simplecov", require: false
|
|
25
|
+
gem "activejob", ">= 7.1.0", "< 9.0"
|
|
26
|
+
gem "actionmailer", ">= 7.1.0", "< 9.0"
|
|
27
|
+
gem "globalid", ">= 1.0"
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
gemspec path: "../"
|
data/gemfiles/rails_8.1.gemfile
CHANGED
|
@@ -7,7 +7,7 @@ gem "rails", "~> 8.1.2"
|
|
|
7
7
|
|
|
8
8
|
group :development do
|
|
9
9
|
gem "irb"
|
|
10
|
-
gem "rubocop", "~> 1.
|
|
10
|
+
gem "rubocop", "~> 1.92"
|
|
11
11
|
gem "rubocop-minitest", "~> 0.39"
|
|
12
12
|
gem "rubocop-performance", "~> 1.26"
|
|
13
13
|
end
|
|
@@ -18,8 +18,12 @@ group :development, :test do
|
|
|
18
18
|
gem "minitest-mock"
|
|
19
19
|
gem "rack-test"
|
|
20
20
|
gem "sqlite3", ">= 2.1"
|
|
21
|
+
gem "pg"
|
|
21
22
|
gem "ostruct"
|
|
22
23
|
gem "simplecov", require: false
|
|
24
|
+
gem "activejob", ">= 7.1.0", "< 9.0"
|
|
25
|
+
gem "actionmailer", ">= 7.1.0", "< 9.0"
|
|
26
|
+
gem "globalid", ">= 1.0"
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
gemspec path: "../"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Organizations
|
|
7
|
+
module Generators
|
|
8
|
+
# Adds the table for scoped resource grants (optional feature):
|
|
9
|
+
#
|
|
10
|
+
# rails g organizations:grants
|
|
11
|
+
# rails db:migrate
|
|
12
|
+
#
|
|
13
|
+
# Then include Organizations::Grantable in the models you grant, and use
|
|
14
|
+
# membership.grant!, membership.can? and Model.visible_to(membership).
|
|
15
|
+
class GrantsGenerator < Rails::Generators::Base
|
|
16
|
+
include ActiveRecord::Generators::Migration
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path("templates", __dir__)
|
|
19
|
+
desc "Add the organizations_grants table (scoped resource grants)"
|
|
20
|
+
|
|
21
|
+
def self.next_migration_number(dir)
|
|
22
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_migration_file
|
|
26
|
+
migration_template "create_organizations_grants.rb.erb",
|
|
27
|
+
File.join(db_migrate_path, "create_organizations_grants.rb"),
|
|
28
|
+
migration_version: migration_version
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def display_post_install_message
|
|
32
|
+
say "\n✅ organizations grants migration created.", :green
|
|
33
|
+
say "\nNext steps:"
|
|
34
|
+
say " 1. Check the type of grantable_id in the migration (it must match your resources' ids)."
|
|
35
|
+
say " 2. Run 'rails db:migrate'."
|
|
36
|
+
say " 3. Add `include Organizations::Grantable` to the models you grant."
|
|
37
|
+
say " 4. See the README section \"Scoped resource grants\"."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def migration_version
|
|
43
|
+
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|