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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +7 -0
  4. data/.simplecov +17 -10
  5. data/CHANGELOG.md +93 -0
  6. data/README.md +687 -33
  7. data/app/controllers/organizations/invitations_controller.rb +27 -22
  8. data/app/controllers/organizations/memberships_controller.rb +37 -30
  9. data/app/controllers/organizations/organizations_controller.rb +8 -8
  10. data/app/controllers/organizations/public_invitations_controller.rb +1 -1
  11. data/app/mailers/organizations/invitation_mailer.rb +2 -2
  12. data/app/mailers/organizations/verification_mailer.rb +1 -1
  13. data/app/models/organizations/grant.rb +6 -0
  14. data/app/views/organizations/invitation_mailer/invitation_email.html.erb +11 -11
  15. data/app/views/organizations/invitation_mailer/invitation_email.text.erb +7 -7
  16. data/app/views/organizations/verification_mailer/code_email.html.erb +3 -3
  17. data/app/views/organizations/verification_mailer/code_email.text.erb +3 -3
  18. data/config/locales/en.yml +22 -0
  19. data/config/locales/es.yml +22 -0
  20. data/config/routes.rb +1 -2
  21. data/gemfiles/rails_7.2.gemfile +5 -1
  22. data/gemfiles/rails_8.1.gemfile +5 -1
  23. data/lib/generators/organizations/grants/grants_generator.rb +47 -0
  24. data/lib/generators/organizations/grants/templates/create_organizations_grants.rb.erb +59 -0
  25. data/lib/generators/organizations/install/install_generator.rb +1 -0
  26. data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +15 -0
  27. data/lib/generators/organizations/join/join_generator.rb +113 -0
  28. data/lib/generators/organizations/join/templates/app/controllers/organization_access_controller.rb +146 -0
  29. data/lib/generators/organizations/join/templates/app/controllers/organization_joins_controller.rb +93 -0
  30. data/lib/generators/organizations/join/templates/app/views/organization_access/show.html.erb +142 -0
  31. data/lib/generators/organizations/join/templates/app/views/organization_joins/_disclosure.html.erb +8 -0
  32. data/lib/generators/organizations/join/templates/app/views/organization_joins/show.html.erb +113 -0
  33. data/lib/generators/organizations/join/templates/routes.rb +19 -0
  34. data/lib/generators/organizations/upgrade/templates/add_domain_verification.rb.erb +29 -0
  35. data/lib/generators/organizations/upgrade/templates/add_personal_organizations.rb.erb +22 -0
  36. data/lib/generators/organizations/upgrade/upgrade_generator.rb +79 -14
  37. data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +1 -1
  38. data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +2 -2
  39. data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +1 -1
  40. data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +21 -13
  41. data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +1 -1
  42. data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +3 -3
  43. data/lib/generators/organizations/views/views_generator.rb +4 -6
  44. data/lib/organizations/account_deletion_plan.rb +118 -0
  45. data/lib/organizations/configuration.rb +152 -0
  46. data/lib/organizations/controller_helpers.rb +55 -15
  47. data/lib/organizations/grant_access.rb +75 -0
  48. data/lib/organizations/grant_holder.rb +152 -0
  49. data/lib/organizations/grantable.rb +28 -0
  50. data/lib/organizations/join_flow.rb +16 -12
  51. data/lib/organizations/models/concerns/has_organizations.rb +217 -9
  52. data/lib/organizations/models/domain.rb +283 -1
  53. data/lib/organizations/models/grant.rb +152 -0
  54. data/lib/organizations/models/invitation.rb +76 -7
  55. data/lib/organizations/models/join_request.rb +102 -43
  56. data/lib/organizations/models/membership.rb +87 -5
  57. data/lib/organizations/models/organization.rb +219 -48
  58. data/lib/organizations/organization_scoped.rb +16 -0
  59. data/lib/organizations/personal_owner_backfill.rb +122 -0
  60. data/lib/organizations/public_email_domains.rb +49 -0
  61. data/lib/organizations/resource.rb +11 -0
  62. data/lib/organizations/roles.rb +55 -0
  63. data/lib/organizations/version.rb +1 -1
  64. data/lib/organizations/view_helpers.rb +40 -9
  65. data/lib/organizations.rb +99 -4
  66. 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
- begin
39
- @invitation = current_user.send_organization_invite_to!(
40
- email,
41
- organization: current_organization,
42
- role: role
43
- )
38
+ @invitation = current_user.send_organization_invite_to!(
39
+ email,
40
+ organization: current_organization,
41
+ role: role
42
+ )
44
43
 
45
- respond_to do |format|
46
- return_to = params[:return_to].presence || organization_invitations_path
47
- format.html { redirect_to return_to, notice: "Invitation sent to #{email}" }
48
- format.json { render json: invitation_json(@invitation), status: :created }
49
- end
50
- rescue ::Organizations::InvitationError, ActiveRecord::RecordInvalid, ArgumentError => e
51
- respond_to do |format|
52
- format.html do
53
- @invitation = current_organization.invitations.build(invitation_params)
54
- flash.now[:alert] = e.message
55
- render :new, status: :unprocessable_entity
56
- end
57
- format.json { render json: { error: e.message }, status: :unprocessable_entity }
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.destroy!
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: :unprocessable_entity }
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
- begin
35
- # Use the invariant-safe domain method that enforces owner rules
36
- current_organization.change_role_of!(
37
- @membership.user,
38
- to: new_role,
39
- changed_by: current_user
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
- # Reload to get updated role
43
- @membership.reload
40
+ # Reload to get updated role
41
+ @membership.reload
44
42
 
45
- respond_to do |format|
46
- format.html { redirect_back fallback_location: memberships_path, notice: "Role updated successfully." }
47
- format.json { render json: membership_json(@membership) }
48
- end
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: :unprocessable_entity }
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: :unprocessable_entity }
60
- end
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: :unprocessable_entity }
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: :unprocessable_entity }
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: :unprocessable_entity }
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: :unprocessable_entity
54
+ render :new, status: 422
55
55
  end
56
- format.json { render json: { error: e.message }, status: :unprocessable_entity }
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: :unprocessable_entity
63
+ render :new, status: 422
64
64
  end
65
- format.json { render json: { errors: e.record.errors }, status: :unprocessable_entity }
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: :unprocessable_entity }
86
- format.json { render json: { errors: @organization.errors }, status: :unprocessable_entity }
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
@@ -76,7 +76,7 @@ module Organizations
76
76
  html_path: main_app.root_path,
77
77
  alert: "Unable to accept this invitation.",
78
78
  json_error: "Acceptance failed",
79
- status: :unprocessable_entity
79
+ status: 422
80
80
  )
81
81
  end
82
82
 
@@ -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.name)
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
@@ -30,7 +30,7 @@ module Organizations
30
30
  mail(
31
31
  to: join_request.verification_email,
32
32
  subject: Organizations.t(:"mailers.verification.subject",
33
- code: @code, organization: @organization.name)
33
+ code: @code, organization: @organization)
34
34
  )
35
35
  end
36
36
 
@@ -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.name) %></title>
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
- <%= t("organizations.mailers.invitation.invited_by_html", inviter: @inviter_name, organization: @organization.name) %>
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
- <%= t("organizations.mailers.invitation.invited_generic_html", organization: @organization.name) %>
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.name) %>
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", inviter: @inviter_name, organization: @organization.name) %><% else %><%= Organizations.t(:"mailers.invitation.invited_generic", organization: @organization.name) %><% end %>
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><%= t("organizations.mailers.verification.lead_html", organization: @organization.name) %></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.name) %>
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.name) %>
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.name) %>
7
+ <%= Organizations.t(:"mailers.verification.ignore", organization: @organization) %>
@@ -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"
@@ -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
@@ -8,7 +8,7 @@ gem "json", "< 3"
8
8
 
9
9
  group :development do
10
10
  gem "irb"
11
- gem "rubocop", "~> 1.88"
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: "../"
@@ -7,7 +7,7 @@ gem "rails", "~> 8.1.2"
7
7
 
8
8
  group :development do
9
9
  gem "irb"
10
- gem "rubocop", "~> 1.88"
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