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.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -9
- data/.rubocop_todo.yml +625 -0
- data/Appraisals +2 -0
- data/CHANGELOG.md +65 -0
- data/README.md +433 -41
- data/app/controllers/organizations/application_controller.rb +4 -0
- data/app/controllers/organizations/memberships_controller.rb +3 -3
- data/app/controllers/organizations/public_controller.rb +26 -0
- data/app/mailers/organizations/invitation_mailer.rb +38 -18
- data/app/mailers/organizations/verification_mailer.rb +56 -0
- data/app/models/organizations/allowlist_entry.rb +6 -0
- data/app/models/organizations/domain.rb +6 -0
- data/app/models/organizations/join_code.rb +6 -0
- data/app/models/organizations/join_request.rb +6 -0
- data/app/views/organizations/invitation_mailer/invitation_email.html.erb +16 -13
- data/app/views/organizations/invitation_mailer/invitation_email.text.erb +8 -8
- data/app/views/organizations/verification_mailer/code_email.html.erb +22 -0
- data/app/views/organizations/verification_mailer/code_email.text.erb +7 -0
- data/config/locales/en.yml +158 -0
- data/config/locales/es.yml +126 -0
- data/config/routes.rb +46 -28
- data/gemfiles/rails_7.2.gemfile +4 -3
- data/gemfiles/rails_8.1.gemfile +3 -3
- data/lib/generators/organizations/install/install_generator.rb +3 -0
- data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +170 -25
- data/lib/generators/organizations/install/templates/initializer.rb +53 -0
- data/lib/generators/organizations/upgrade/templates/add_verified_joining_to_organizations.rb.erb +180 -0
- data/lib/generators/organizations/upgrade/upgrade_generator.rb +50 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +51 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +112 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/new.html.erb +10 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +99 -0
- data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +87 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/_form.html.erb +39 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/edit.html.erb +10 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +93 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/new.html.erb +10 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +270 -0
- data/lib/generators/organizations/views/views_generator.rb +47 -0
- data/lib/organizations/callback_context.rb +9 -0
- data/lib/organizations/callbacks.rb +26 -18
- data/lib/organizations/configuration.rb +278 -3
- data/lib/organizations/controller_helpers.rb +38 -12
- data/lib/organizations/email_normalizer.rb +65 -0
- data/lib/organizations/join_flow.rb +227 -0
- data/lib/organizations/join_state.rb +117 -0
- data/lib/organizations/metadata_flags.rb +79 -0
- data/lib/organizations/models/allowlist_entry.rb +88 -0
- data/lib/organizations/models/concerns/has_organizations.rb +100 -12
- data/lib/organizations/models/domain.rb +85 -0
- data/lib/organizations/models/invitation.rb +108 -18
- data/lib/organizations/models/join_code.rb +242 -0
- data/lib/organizations/models/join_request.rb +627 -0
- data/lib/organizations/models/membership.rb +48 -18
- data/lib/organizations/models/organization.rb +317 -19
- data/lib/organizations/organization_scoped.rb +135 -0
- data/lib/organizations/roles.rb +143 -36
- data/lib/organizations/test_helpers.rb +35 -1
- data/lib/organizations/version.rb +1 -1
- data/lib/organizations/view_helpers.rb +8 -12
- data/lib/organizations.rb +165 -0
- 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
|
+
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-
|
|
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
|