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.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -9
- data/.rubocop_todo.yml +625 -0
- data/CHANGELOG.md +46 -0
- data/README.md +405 -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 +3 -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 +275 -1
- 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 +105 -17
- data/lib/organizations/models/join_code.rb +242 -0
- data/lib/organizations/models/join_request.rb +627 -0
- data/lib/organizations/models/membership.rb +39 -8
- data/lib/organizations/models/organization.rb +317 -19
- data/lib/organizations/organization_scoped.rb +135 -0
- 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
|
@@ -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
|
-
|
|
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
|
|
@@ -101,13 +101,11 @@ module Organizations
|
|
|
101
101
|
# organization_role_label(:admin) # => "Admin"
|
|
102
102
|
#
|
|
103
103
|
def organization_role_label(role)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
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-
|
|
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
|