organizations 0.4.3 → 0.5.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -9
  3. data/.rubocop_todo.yml +625 -0
  4. data/CHANGELOG.md +46 -0
  5. data/README.md +405 -41
  6. data/app/controllers/organizations/application_controller.rb +4 -0
  7. data/app/controllers/organizations/memberships_controller.rb +3 -3
  8. data/app/controllers/organizations/public_controller.rb +26 -0
  9. data/app/mailers/organizations/invitation_mailer.rb +38 -18
  10. data/app/mailers/organizations/verification_mailer.rb +56 -0
  11. data/app/models/organizations/allowlist_entry.rb +6 -0
  12. data/app/models/organizations/domain.rb +6 -0
  13. data/app/models/organizations/join_code.rb +6 -0
  14. data/app/models/organizations/join_request.rb +6 -0
  15. data/app/views/organizations/invitation_mailer/invitation_email.html.erb +16 -13
  16. data/app/views/organizations/invitation_mailer/invitation_email.text.erb +8 -8
  17. data/app/views/organizations/verification_mailer/code_email.html.erb +22 -0
  18. data/app/views/organizations/verification_mailer/code_email.text.erb +7 -0
  19. data/config/locales/en.yml +158 -0
  20. data/config/locales/es.yml +126 -0
  21. data/config/routes.rb +46 -28
  22. data/gemfiles/rails_7.2.gemfile +3 -3
  23. data/gemfiles/rails_8.1.gemfile +3 -3
  24. data/lib/generators/organizations/install/install_generator.rb +3 -0
  25. data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +170 -25
  26. data/lib/generators/organizations/install/templates/initializer.rb +53 -0
  27. data/lib/generators/organizations/upgrade/templates/add_verified_joining_to_organizations.rb.erb +180 -0
  28. data/lib/generators/organizations/upgrade/upgrade_generator.rb +50 -0
  29. data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +51 -0
  30. data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +112 -0
  31. data/lib/generators/organizations/views/templates/organizations/invitations/new.html.erb +10 -0
  32. data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +99 -0
  33. data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +87 -0
  34. data/lib/generators/organizations/views/templates/organizations/organizations/_form.html.erb +39 -0
  35. data/lib/generators/organizations/views/templates/organizations/organizations/edit.html.erb +10 -0
  36. data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +93 -0
  37. data/lib/generators/organizations/views/templates/organizations/organizations/new.html.erb +10 -0
  38. data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +270 -0
  39. data/lib/generators/organizations/views/views_generator.rb +47 -0
  40. data/lib/organizations/callback_context.rb +9 -0
  41. data/lib/organizations/callbacks.rb +26 -18
  42. data/lib/organizations/configuration.rb +275 -1
  43. data/lib/organizations/controller_helpers.rb +38 -12
  44. data/lib/organizations/email_normalizer.rb +65 -0
  45. data/lib/organizations/join_flow.rb +227 -0
  46. data/lib/organizations/join_state.rb +117 -0
  47. data/lib/organizations/metadata_flags.rb +79 -0
  48. data/lib/organizations/models/allowlist_entry.rb +88 -0
  49. data/lib/organizations/models/concerns/has_organizations.rb +100 -12
  50. data/lib/organizations/models/domain.rb +85 -0
  51. data/lib/organizations/models/invitation.rb +105 -17
  52. data/lib/organizations/models/join_code.rb +242 -0
  53. data/lib/organizations/models/join_request.rb +627 -0
  54. data/lib/organizations/models/membership.rb +39 -8
  55. data/lib/organizations/models/organization.rb +317 -19
  56. data/lib/organizations/organization_scoped.rb +135 -0
  57. data/lib/organizations/test_helpers.rb +35 -1
  58. data/lib/organizations/version.rb +1 -1
  59. data/lib/organizations/view_helpers.rb +8 -12
  60. data/lib/organizations.rb +165 -0
  61. metadata +34 -2
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Organizations
4
+ # URL-SCOPED organization resolution — the second addressing mode.
5
+ #
6
+ # The engine's own controllers are SESSION-scoped (one "current"
7
+ # organization per user, workspace/tenant style — the LicenseSeat shape).
8
+ # Overlay-style hosts (community/marketplace apps where users belong to
9
+ # organizations but don't "work inside" one) address organizations BY URL
10
+ # instead: /org/:slug/admin, /teams/:id/settings. Before this concern,
11
+ # such hosts re-derived the same base controller by hand: find org by
12
+ # param → find viewer membership → role-gate → pick a not-found posture.
13
+ #
14
+ # @example A slug-addressed admin portal (the overlay shape)
15
+ # class Portal::BaseController < ApplicationController
16
+ # include Organizations::OrganizationScoped
17
+ #
18
+ # self.organization_param = :slug
19
+ # self.organization_finder = ->(param) { Organizations::Organization.find_by(slug: param) }
20
+ # # 404-never-403: don't disclose which orgs/surfaces exist (default).
21
+ # require_organization_role :admin
22
+ # end
23
+ #
24
+ # class Portal::MembersController < Portal::BaseController
25
+ # def index
26
+ # @memberships = current_scoped_organization.memberships.includes(:user)
27
+ # end
28
+ # end
29
+ #
30
+ # Knobs (class_attributes, inheritable/overridable per controller):
31
+ # organization_param — request param holding the identifier
32
+ # (default :organization_id)
33
+ # organization_finder — ->(param) { ... } returning the org or nil;
34
+ # instance_exec'd on the controller (default
35
+ # find_by(id:))
36
+ # organization_not_found_behavior — :not_found (default) raises
37
+ # ActionController::RoutingError so unknown orgs,
38
+ # non-members, and under-role members are all
39
+ # INDISTINGUISHABLE (no existence oracle);
40
+ # :forbidden responds 403 instead (internal
41
+ # tools where disclosure is fine).
42
+ #
43
+ # Deliberately DISTINCT from the session helpers (current_organization/
44
+ # current_membership): the two modes coexist — a session-tenant app can
45
+ # still expose a URL-scoped surface — so this concern never touches the
46
+ # session or the model-level current-organization context.
47
+ module OrganizationScoped
48
+ extend ActiveSupport::Concern
49
+ include Organizations::CurrentUserResolution
50
+
51
+ included do
52
+ class_attribute :organization_param, default: :organization_id, instance_writer: false
53
+ class_attribute :organization_finder,
54
+ default: ->(param) { Organizations::Organization.find_by(id: param) },
55
+ instance_writer: false
56
+ class_attribute :organization_not_found_behavior, default: :not_found, instance_writer: false
57
+
58
+ before_action :set_scoped_organization if respond_to?(:before_action)
59
+
60
+ if respond_to?(:helper_method)
61
+ helper_method :current_scoped_organization
62
+ helper_method :current_scoped_membership
63
+ end
64
+ end
65
+
66
+ class_methods do
67
+ # Gate every action (or only:/except: subsets, forwarded to
68
+ # before_action) behind a minimum role in the scoped organization.
69
+ # Below-role viewers get the configured not-found behavior — same
70
+ # posture as an unknown organization, on purpose.
71
+ #
72
+ # @param role [Symbol] minimum role (uses the gem hierarchy /
73
+ # custom-role at_least semantics)
74
+ def require_organization_role(role, **options)
75
+ before_action(**options) { require_scoped_organization_role!(role) }
76
+ end
77
+ end
78
+
79
+ # The organization resolved from the URL for this request.
80
+ # @return [Organizations::Organization, nil]
81
+ def current_scoped_organization
82
+ @current_scoped_organization
83
+ end
84
+
85
+ # The requesting user's membership in the scoped organization.
86
+ # Memoized per request; nil for strangers/signed-out.
87
+ # @return [Organizations::Membership, nil]
88
+ def current_scoped_membership
89
+ return @current_scoped_membership if defined?(@current_scoped_membership)
90
+
91
+ user = scoped_organization_viewer
92
+ @current_scoped_membership =
93
+ (current_scoped_organization.memberships.find_by(user_id: user.id) if user && current_scoped_organization)
94
+ end
95
+
96
+ private
97
+
98
+ # The requesting user via the shared resolution (configured
99
+ # current_user_method, Warden fallback) — own cache ivar so it never
100
+ # collides with ControllerHelpers' memoization when both are included.
101
+ def scoped_organization_viewer
102
+ resolve_organizations_current_user(
103
+ cache_ivar: :@_scoped_organization_viewer,
104
+ prefer_super_for_current_user: false,
105
+ prefer_warden_for_current_user: true
106
+ )
107
+ end
108
+
109
+ def set_scoped_organization
110
+ finder = self.class.organization_finder
111
+ @current_scoped_organization = instance_exec(params[self.class.organization_param], &finder)
112
+
113
+ scoped_organization_not_found! unless @current_scoped_organization
114
+ end
115
+
116
+ def require_scoped_organization_role!(role)
117
+ membership = current_scoped_membership
118
+ return if membership&.is_at_least?(role)
119
+
120
+ scoped_organization_not_found!
121
+ end
122
+
123
+ def scoped_organization_not_found!
124
+ case self.class.organization_not_found_behavior
125
+ when :forbidden
126
+ head :forbidden
127
+ else
128
+ # RoutingError renders the host's 404 page — chosen over head(:not_found)
129
+ # so the user sees the app's normal not-found experience, and over 403
130
+ # so existence is never disclosed.
131
+ raise ActionController::RoutingError, "Not Found"
132
+ end
133
+ end
134
+ end
135
+ end
@@ -111,7 +111,9 @@ module Organizations
111
111
  # @param role [Symbol] Role in the organization
112
112
  # @return [User]
113
113
  def create_user(email: "user@example.com", name: "Test User", org: nil, role: :member)
114
- user = User.create!(email: email, name: name)
114
+ # Respects config.user_class so hosts with a differently-named account
115
+ # model can use this helper unchanged.
116
+ user = Organizations.user_class.create!(email: email, name: name)
115
117
 
116
118
  if org
117
119
  Organizations::Membership.create!(
@@ -124,6 +126,38 @@ module Organizations
124
126
  user
125
127
  end
126
128
 
129
+ # Force a KNOWN verification code onto a join request's active challenge,
130
+ # so tests can complete the emailed-code flow without intercepting mail.
131
+ #
132
+ # The database only ever stores a digest (SHA-256, peppered by row id) —
133
+ # before this helper, every host test suite reverse-engineered that
134
+ # recipe by hand (overwriting verification_code_digest with
135
+ # digest_verification_code). That's gem INTERNALS leaking into host
136
+ # tests; use this instead:
137
+ #
138
+ # request.start_email_verification!(email: "j.doe@acme.com")
139
+ # code = issue_verification_code(request) # => "424242"
140
+ # request.verify_email_code!(code) # => Membership
141
+ #
142
+ # Also callable WITHOUT including the module —
143
+ # Organizations::TestHelpers.issue_verification_code(request)
144
+ # — for suites whose own factory names (create_user, …) would collide
145
+ # with this module's (a real host hit exactly that).
146
+ #
147
+ # @param join_request [Organizations::JoinRequest] with a challenge started
148
+ # @param code [String] the plaintext code to force (default "424242")
149
+ # @return [String] the plaintext code, for typing into the flow
150
+ def issue_verification_code(join_request, code: "424242")
151
+ join_request.update!(
152
+ verification_code_digest: Organizations::JoinRequest.digest_verification_code(code, join_request.id)
153
+ )
154
+ code
155
+ end
156
+ module_function :issue_verification_code
157
+ # module_function makes the INSTANCE copy private — fine for included
158
+ # test usage (implicit receiver) while enabling the module-level call.
159
+ public :issue_verification_code
160
+
127
161
  # Assert that a user is a member of an organization
128
162
  # @param user [User] The user
129
163
  # @param org [Organizations::Organization] The organization
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Organizations
4
- VERSION = "0.4.3"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -101,13 +101,11 @@ module Organizations
101
101
  # organization_role_label(:admin) # => "Admin"
102
102
  #
103
103
  def organization_role_label(role)
104
- case role.to_sym
105
- when :owner then "Owner"
106
- when :admin then "Admin"
107
- when :member then "Member"
108
- when :viewer then "Viewer"
109
- else role.to_s.humanize
110
- end
104
+ # I18n-backed (organizations.roles.*) so hosts localize/retheme labels
105
+ # by overriding locale keys instead of monkey-patching this helper.
106
+ # Custom roles defined via config.roles fall back to humanize unless
107
+ # the host adds a matching organizations.roles.<name> key.
108
+ Organizations.t(:"roles.#{role}", default: role.to_s.humanize)
111
109
  end
112
110
 
113
111
  # Returns a hash of role information for building badges
@@ -143,11 +141,9 @@ module Organizations
143
141
  # @param invitation [Organizations::Invitation] The invitation
144
142
  # @return [String]
145
143
  def organization_invitation_status_label(invitation)
146
- case organization_invitation_status(invitation)
147
- when :pending then "Pending"
148
- when :accepted then "Accepted"
149
- when :expired then "Expired"
150
- end
144
+ status = organization_invitation_status(invitation)
145
+ # I18n-backed (organizations.invitation_status.*) — see role labels.
146
+ Organizations.t(:"invitation_status.#{status}", default: status.to_s.humanize)
151
147
  end
152
148
 
153
149
  # Returns a hash of invitation status information
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.5.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-07-22 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