organizations 0.4.3 → 0.6.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -9
  3. data/.rubocop_todo.yml +625 -0
  4. data/Appraisals +2 -0
  5. data/CHANGELOG.md +65 -0
  6. data/README.md +433 -41
  7. data/app/controllers/organizations/application_controller.rb +4 -0
  8. data/app/controllers/organizations/memberships_controller.rb +3 -3
  9. data/app/controllers/organizations/public_controller.rb +26 -0
  10. data/app/mailers/organizations/invitation_mailer.rb +38 -18
  11. data/app/mailers/organizations/verification_mailer.rb +56 -0
  12. data/app/models/organizations/allowlist_entry.rb +6 -0
  13. data/app/models/organizations/domain.rb +6 -0
  14. data/app/models/organizations/join_code.rb +6 -0
  15. data/app/models/organizations/join_request.rb +6 -0
  16. data/app/views/organizations/invitation_mailer/invitation_email.html.erb +16 -13
  17. data/app/views/organizations/invitation_mailer/invitation_email.text.erb +8 -8
  18. data/app/views/organizations/verification_mailer/code_email.html.erb +22 -0
  19. data/app/views/organizations/verification_mailer/code_email.text.erb +7 -0
  20. data/config/locales/en.yml +158 -0
  21. data/config/locales/es.yml +126 -0
  22. data/config/routes.rb +46 -28
  23. data/gemfiles/rails_7.2.gemfile +4 -3
  24. data/gemfiles/rails_8.1.gemfile +3 -3
  25. data/lib/generators/organizations/install/install_generator.rb +3 -0
  26. data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +170 -25
  27. data/lib/generators/organizations/install/templates/initializer.rb +53 -0
  28. data/lib/generators/organizations/upgrade/templates/add_verified_joining_to_organizations.rb.erb +180 -0
  29. data/lib/generators/organizations/upgrade/upgrade_generator.rb +50 -0
  30. data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +51 -0
  31. data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +112 -0
  32. data/lib/generators/organizations/views/templates/organizations/invitations/new.html.erb +10 -0
  33. data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +99 -0
  34. data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +87 -0
  35. data/lib/generators/organizations/views/templates/organizations/organizations/_form.html.erb +39 -0
  36. data/lib/generators/organizations/views/templates/organizations/organizations/edit.html.erb +10 -0
  37. data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +93 -0
  38. data/lib/generators/organizations/views/templates/organizations/organizations/new.html.erb +10 -0
  39. data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +270 -0
  40. data/lib/generators/organizations/views/views_generator.rb +47 -0
  41. data/lib/organizations/callback_context.rb +9 -0
  42. data/lib/organizations/callbacks.rb +26 -18
  43. data/lib/organizations/configuration.rb +278 -3
  44. data/lib/organizations/controller_helpers.rb +38 -12
  45. data/lib/organizations/email_normalizer.rb +65 -0
  46. data/lib/organizations/join_flow.rb +227 -0
  47. data/lib/organizations/join_state.rb +117 -0
  48. data/lib/organizations/metadata_flags.rb +79 -0
  49. data/lib/organizations/models/allowlist_entry.rb +88 -0
  50. data/lib/organizations/models/concerns/has_organizations.rb +100 -12
  51. data/lib/organizations/models/domain.rb +85 -0
  52. data/lib/organizations/models/invitation.rb +108 -18
  53. data/lib/organizations/models/join_code.rb +242 -0
  54. data/lib/organizations/models/join_request.rb +627 -0
  55. data/lib/organizations/models/membership.rb +48 -18
  56. data/lib/organizations/models/organization.rb +317 -19
  57. data/lib/organizations/organization_scoped.rb +135 -0
  58. data/lib/organizations/roles.rb +143 -36
  59. data/lib/organizations/test_helpers.rb +35 -1
  60. data/lib/organizations/version.rb +1 -1
  61. data/lib/organizations/view_helpers.rb +8 -12
  62. data/lib/organizations.rb +165 -0
  63. metadata +34 -2
data/lib/organizations.rb CHANGED
@@ -41,6 +41,52 @@ module Organizations
41
41
  class InvitationAlreadyAccepted < InvitationError; end
42
42
  class InvitationEmailMismatch < InvitationError; end
43
43
 
44
+ # User-level membership errors — canonical TOP-LEVEL homes (0.5.0). These
45
+ # were historically defined four modules deep inside the HasOrganizations
46
+ # concern, forcing hosts to rescue
47
+ # Organizations::Models::Concerns::HasOrganizations::CannotLeaveAsLastOwner
48
+ # — a real papercut found in a production host. The nested constants are
49
+ # kept as aliases; rescue THESE.
50
+ class OrganizationLimitReached < Error; end
51
+ class CannotLeaveLastOrganization < Error; end
52
+ class CannotLeaveAsLastOwner < Error; end
53
+ class CannotDeleteAsOrganizationOwner < Error; end
54
+ class NoCurrentOrganization < Error; end
55
+
56
+ # Join request errors (verified joining)
57
+ class JoinRequestError < Error; end
58
+ class JoinRequestExpired < JoinRequestError; end
59
+ class JoinRequestAlreadyDecided < JoinRequestError; end
60
+
61
+ # Raised (typically by the host's `on_member_joining` gate) to VETO a
62
+ # membership that is about to be created — seat limits, member caps,
63
+ # compliance holds. The gate dispatches strictly inside the creating
64
+ # transaction, so raising this rolls everything back cleanly: no membership
65
+ # row, join requests stay pending (resumable), invitations stay unaccepted.
66
+ # Raising without a message gets the localized default
67
+ # (organizations.errors.membership_vetoed).
68
+ class MembershipVetoed < Error
69
+ def initialize(message = nil)
70
+ super(message || Organizations.t(:"errors.membership_vetoed"))
71
+ end
72
+ end
73
+
74
+ # Join code errors (verified joining)
75
+ # JoinCodeInvalid covers unknown, revoked, and expired codes — hosts should
76
+ # show the same generic message for all three (don't leak which codes exist).
77
+ class JoinCodeError < Error; end
78
+ class JoinCodeInvalid < JoinCodeError; end
79
+ class JoinCodeExhausted < JoinCodeInvalid; end
80
+
81
+ # Email verification errors (verified joining)
82
+ class VerificationError < JoinRequestError; end
83
+ class VerificationEmailNotEligible < VerificationError; end
84
+ class VerificationEmailAlreadyClaimed < VerificationError; end
85
+ class VerificationCodeInvalid < VerificationError; end
86
+ class VerificationCodeExpired < VerificationError; end
87
+ class VerificationAttemptsExceeded < VerificationError; end
88
+ class VerificationThrottled < VerificationError; end
89
+
44
90
  # === Autoload Components (lazy loading) ===
45
91
 
46
92
  autoload :Configuration, "organizations/configuration"
@@ -54,6 +100,11 @@ module Organizations
54
100
  autoload :CurrentUserResolution, "organizations/current_user_resolution"
55
101
  autoload :InvitationAcceptanceResult, "organizations/invitation_acceptance_result"
56
102
  autoload :InvitationAcceptanceFailure, "organizations/invitation_acceptance_failure"
103
+ autoload :EmailNormalizer, "organizations/email_normalizer"
104
+ autoload :JoinFlow, "organizations/join_flow"
105
+ autoload :JoinState, "organizations/join_state"
106
+ autoload :MetadataFlags, "organizations/metadata_flags"
107
+ autoload :OrganizationScoped, "organizations/organization_scoped"
57
108
 
58
109
  # Alias for README compatibility: `include Organizations::Controller`
59
110
  Controller = ControllerHelpers
@@ -87,6 +138,19 @@ module Organizations
87
138
  autoload :Organization, "organizations/models/organization"
88
139
  autoload :Membership, "organizations/models/membership"
89
140
  autoload :Invitation, "organizations/models/invitation"
141
+ autoload :Domain, "organizations/models/domain"
142
+ autoload :JoinCode, "organizations/models/join_code"
143
+ autoload :AllowlistEntry, "organizations/models/allowlist_entry"
144
+ autoload :JoinRequest, "organizations/models/join_request"
145
+
146
+ # Non-Rails contexts (plain ActiveRecord, the gem's own test suite) don't
147
+ # get the Rails engine's automatic `config/locales` pickup, so register
148
+ # the gem's locale files directly. `|=` keeps this idempotent. In Rails
149
+ # apps the engine handles it (Rails::Engine adds paths["config/locales"]
150
+ # to I18n.load_path — https://guides.rubyonrails.org/engines.html).
151
+ # I18n itself is always available: it's a hard dependency of
152
+ # activesupport, which this gem depends on.
153
+ I18n.load_path |= Dir[File.expand_path("../config/locales/*.yml", __dir__)]
90
154
  end
91
155
 
92
156
  class << self
@@ -107,22 +171,123 @@ module Organizations
107
171
  # config.invitation_expiry = 7.days
108
172
  # end
109
173
  #
174
+ # ATOMIC on validation failure: a configure block that raises (typically
175
+ # via validate!) restores the previous configuration instead of leaving
176
+ # half-applied settings on the live object. Found the hard way: a
177
+ # validation-failure left its invalid assignment behind on the shared
178
+ # config, and every LATER configure call re-raised that stale error —
179
+ # an order-dependent heisenbug in test suites and a real hazard for
180
+ # hosts rescuing ConfigurationError in initializers.
181
+ # NOTE: restoration is a shallow dup — use SETTERS in configure blocks
182
+ # (config.x = [...]), never in-place mutation (config.x << ...), which
183
+ # is the documented contract anyway.
110
184
  def configure
185
+ snapshot = configuration.dup
111
186
  yield(configuration)
112
187
  configuration.validate!
188
+ rescue StandardError
189
+ @configuration = snapshot
190
+ raise
113
191
  end
114
192
 
115
193
  # Reset configuration to defaults
116
194
  # Primarily used in tests
117
195
  def reset_configuration!
118
196
  @configuration = nil
197
+ remove_instance_variable(:@engine_mount_path) if defined?(@engine_mount_path)
119
198
  Roles.reset!
120
199
  end
121
200
 
201
+ # The path prefix the engine is mounted at in the HOST app ("" when
202
+ # mounted at root, "/orgs" when mounted there, "" outside Rails).
203
+ #
204
+ # Why this exists: engine route helpers called WITHOUT a controller
205
+ # context (from models/mailers — e.g. building an invitation acceptance
206
+ # URL) don't know the mount point, so URLs built as
207
+ # "/invitations/<token>" silently 404 for any host that mounts the
208
+ # engine anywhere but root. Both known hosts mount at root, which is
209
+ # exactly why nobody noticed. Memoized — the mount point is fixed at
210
+ # boot. Source on mounted helpers vs raw engine url_helpers:
211
+ # https://guides.rubyonrails.org/engines.html#routes
212
+ # @return [String]
213
+ def engine_mount_path
214
+ return @engine_mount_path if defined?(@engine_mount_path)
215
+
216
+ @engine_mount_path = compute_engine_mount_path
217
+ end
218
+
219
+ # @api private
220
+ def compute_engine_mount_path
221
+ routes = host_application_routes
222
+ return "" unless routes && defined?(Organizations::Engine)
223
+
224
+ mount = routes.routes.detect do |route|
225
+ route.app.respond_to?(:app) && route.app.app == Organizations::Engine
226
+ end
227
+ return "" unless mount
228
+
229
+ # "/orgs(.:format)" → "/orgs"; a root mount "/" → "".
230
+ mount.path.spec.to_s.delete_suffix("(.:format)").chomp("/")
231
+ rescue StandardError
232
+ ""
233
+ end
234
+
235
+ # True only under a real, booted Rails app — the ONE home for this guard
236
+ # (mailers and URL builders all need it). ⚠️ `defined?(Rails)` alone is
237
+ # NOT enough: several gems define a bare `Rails` module WITHOUT
238
+ # `.application` (globalid setups, railtie fragments, bare test
239
+ # harnesses), where `Rails.application` raises NoMethodError instead of
240
+ # returning nil. Found by the first tests to ever render the stock mails.
241
+ def full_rails_app?
242
+ !!(defined?(Rails) && Rails.respond_to?(:application) && Rails.application)
243
+ end
244
+
245
+ # @api private
246
+ def host_application_routes
247
+ return nil unless full_rails_app?
248
+
249
+ Rails.application.routes
250
+ end
251
+
122
252
  # Get the roles module
123
253
  # @return [Module]
124
254
  def roles
125
255
  Roles
126
256
  end
257
+
258
+ # The host's user model class name (config.user_class, default "User").
259
+ # Used as `class_name:` at association-definition time in the gem's
260
+ # models — they load AFTER initializers in Rails (Zeitwerk shims) and on
261
+ # first constant reference in plain Ruby, so a configured value is
262
+ # visible as long as hosts configure before touching the models.
263
+ # @return [String]
264
+ def user_class_name
265
+ configuration.user_class
266
+ end
267
+
268
+ # The host's user model class (constantized user_class_name).
269
+ # @return [Class]
270
+ def user_class
271
+ user_class_name.constantize
272
+ end
273
+
274
+ # Resolve a gem string through I18n under the `organizations.` namespace.
275
+ # This is the ONE door every user-facing string the gem produces goes
276
+ # through — error messages, labels, mailer copy. en.yml is the catalog
277
+ # SSOT (no inline English defaults on purpose: a missing key renders as
278
+ # "Translation missing: …", a loud and findable bug, instead of silently
279
+ # drifting from the catalog).
280
+ #
281
+ # Hosts override any key the standard Rails way — app locale files load
282
+ # after engine locale files, so the host's value wins.
283
+ #
284
+ # @param key [String, Symbol] key under the `organizations.` scope,
285
+ # e.g. :"errors.join_code_invalid" or "roles.owner"
286
+ # @param options [Hash] I18n options (interpolations, :locale, :default…)
287
+ # @return [String]
288
+ def translate(key, **)
289
+ I18n.t(key, scope: :organizations, **)
290
+ end
291
+ alias t translate
127
292
  end
128
293
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: organizations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-03-19 00:00:00.000000000 Z
10
+ date: 2026-10-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -81,6 +81,7 @@ extensions: []
81
81
  extra_rdoc_files: []
82
82
  files:
83
83
  - ".rubocop.yml"
84
+ - ".rubocop_todo.yml"
84
85
  - ".simplecov"
85
86
  - AGENTS.md
86
87
  - Appraisals
@@ -97,11 +98,20 @@ files:
97
98
  - app/controllers/organizations/public_invitations_controller.rb
98
99
  - app/controllers/organizations/switch_controller.rb
99
100
  - app/mailers/organizations/invitation_mailer.rb
101
+ - app/mailers/organizations/verification_mailer.rb
102
+ - app/models/organizations/allowlist_entry.rb
103
+ - app/models/organizations/domain.rb
100
104
  - app/models/organizations/invitation.rb
105
+ - app/models/organizations/join_code.rb
106
+ - app/models/organizations/join_request.rb
101
107
  - app/models/organizations/membership.rb
102
108
  - app/models/organizations/organization.rb
103
109
  - app/views/organizations/invitation_mailer/invitation_email.html.erb
104
110
  - app/views/organizations/invitation_mailer/invitation_email.text.erb
111
+ - app/views/organizations/verification_mailer/code_email.html.erb
112
+ - app/views/organizations/verification_mailer/code_email.text.erb
113
+ - config/locales/en.yml
114
+ - config/locales/es.yml
105
115
  - config/routes.rb
106
116
  - context7.json
107
117
  - docs/organizations-invitation-accept-create-account.webp
@@ -113,6 +123,19 @@ files:
113
123
  - lib/generators/organizations/install/install_generator.rb
114
124
  - lib/generators/organizations/install/templates/create_organizations_tables.rb.erb
115
125
  - lib/generators/organizations/install/templates/initializer.rb
126
+ - lib/generators/organizations/upgrade/templates/add_verified_joining_to_organizations.rb.erb
127
+ - lib/generators/organizations/upgrade/upgrade_generator.rb
128
+ - lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb
129
+ - lib/generators/organizations/views/templates/organizations/invitations/index.html.erb
130
+ - lib/generators/organizations/views/templates/organizations/invitations/new.html.erb
131
+ - lib/generators/organizations/views/templates/organizations/invitations/show.html.erb
132
+ - lib/generators/organizations/views/templates/organizations/memberships/index.html.erb
133
+ - lib/generators/organizations/views/templates/organizations/organizations/_form.html.erb
134
+ - lib/generators/organizations/views/templates/organizations/organizations/edit.html.erb
135
+ - lib/generators/organizations/views/templates/organizations/organizations/index.html.erb
136
+ - lib/generators/organizations/views/templates/organizations/organizations/new.html.erb
137
+ - lib/generators/organizations/views/templates/organizations/organizations/show.html.erb
138
+ - lib/generators/organizations/views/views_generator.rb
116
139
  - lib/organizations.rb
117
140
  - lib/organizations/acts_as_tenant_integration.rb
118
141
  - lib/organizations/callback_context.rb
@@ -120,13 +143,22 @@ files:
120
143
  - lib/organizations/configuration.rb
121
144
  - lib/organizations/controller_helpers.rb
122
145
  - lib/organizations/current_user_resolution.rb
146
+ - lib/organizations/email_normalizer.rb
123
147
  - lib/organizations/engine.rb
124
148
  - lib/organizations/invitation_acceptance_failure.rb
125
149
  - lib/organizations/invitation_acceptance_result.rb
150
+ - lib/organizations/join_flow.rb
151
+ - lib/organizations/join_state.rb
152
+ - lib/organizations/metadata_flags.rb
153
+ - lib/organizations/models/allowlist_entry.rb
126
154
  - lib/organizations/models/concerns/has_organizations.rb
155
+ - lib/organizations/models/domain.rb
127
156
  - lib/organizations/models/invitation.rb
157
+ - lib/organizations/models/join_code.rb
158
+ - lib/organizations/models/join_request.rb
128
159
  - lib/organizations/models/membership.rb
129
160
  - lib/organizations/models/organization.rb
161
+ - lib/organizations/organization_scoped.rb
130
162
  - lib/organizations/roles.rb
131
163
  - lib/organizations/test_helpers.rb
132
164
  - lib/organizations/version.rb