organizations 0.1.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 +7 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +35 -0
- data/AGENTS.md +5 -0
- data/Appraisals +9 -0
- data/CHANGELOG.md +14 -0
- data/CLAUDE.md +5 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +1496 -0
- data/Rakefile +15 -0
- data/app/controllers/organizations/application_controller.rb +251 -0
- data/app/controllers/organizations/invitations_controller.rb +262 -0
- data/app/controllers/organizations/memberships_controller.rb +179 -0
- data/app/controllers/organizations/organizations_controller.rb +179 -0
- data/app/controllers/organizations/switch_controller.rb +38 -0
- data/app/mailers/organizations/invitation_mailer.rb +85 -0
- data/app/views/organizations/invitation_mailer/invitation_email.html.erb +98 -0
- data/app/views/organizations/invitation_mailer/invitation_email.text.erb +18 -0
- data/config/routes.rb +35 -0
- data/examples/demo_insert_race_condition.rb +212 -0
- data/examples/demo_slugifiable_integration.rb +350 -0
- data/lib/generators/organizations/install/install_generator.rb +42 -0
- data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +128 -0
- data/lib/generators/organizations/install/templates/initializer.rb +83 -0
- data/lib/organizations/acts_as_tenant_integration.rb +54 -0
- data/lib/organizations/callback_context.rb +51 -0
- data/lib/organizations/callbacks.rb +120 -0
- data/lib/organizations/configuration.rb +286 -0
- data/lib/organizations/controller_helpers.rb +292 -0
- data/lib/organizations/engine.rb +65 -0
- data/lib/organizations/models/concerns/has_organizations.rb +509 -0
- data/lib/organizations/models/invitation.rb +295 -0
- data/lib/organizations/models/membership.rb +260 -0
- data/lib/organizations/models/organization.rb +451 -0
- data/lib/organizations/roles.rb +256 -0
- data/lib/organizations/test_helpers.rb +167 -0
- data/lib/organizations/version.rb +5 -0
- data/lib/organizations/view_helpers.rb +353 -0
- data/lib/organizations.rb +107 -0
- metadata +163 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Organizations
|
|
4
|
+
# Immutable context object passed to all callbacks.
|
|
5
|
+
# Provides consistent, typed access to event data.
|
|
6
|
+
#
|
|
7
|
+
# Different events populate different fields:
|
|
8
|
+
# - :organization_created => organization, user
|
|
9
|
+
# - :member_invited => organization, invitation, invited_by
|
|
10
|
+
# - :member_joined => organization, membership, user
|
|
11
|
+
# - :member_removed => organization, membership, user, removed_by
|
|
12
|
+
# - :role_changed => organization, membership, old_role, new_role, changed_by
|
|
13
|
+
# - :ownership_transferred => organization, old_owner, new_owner
|
|
14
|
+
#
|
|
15
|
+
# @example Accessing context data
|
|
16
|
+
# config.on_organization_created do |ctx|
|
|
17
|
+
# Analytics.track(ctx.user, "org_created", name: ctx.organization.name)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
CallbackContext = Struct.new(
|
|
21
|
+
:event, # Symbol - the event type (:organization_created, :member_joined, etc.)
|
|
22
|
+
:organization, # Organizations::Organization instance
|
|
23
|
+
:user, # User instance (the subject of the action)
|
|
24
|
+
:membership, # Organizations::Membership instance (if applicable)
|
|
25
|
+
:invitation, # Organizations::Invitation instance (if applicable)
|
|
26
|
+
:invited_by, # User instance - who sent the invitation
|
|
27
|
+
:removed_by, # User instance - who removed the member
|
|
28
|
+
:changed_by, # User instance - who changed the role
|
|
29
|
+
:old_role, # Symbol - previous role (for role_changed)
|
|
30
|
+
:new_role, # Symbol - new role (for role_changed)
|
|
31
|
+
:old_owner, # User instance - previous owner (for ownership_transferred)
|
|
32
|
+
:new_owner, # User instance - new owner (for ownership_transferred)
|
|
33
|
+
:permission, # Symbol - the permission that was required (for unauthorized)
|
|
34
|
+
:required_role, # Symbol - the role that was required (for unauthorized)
|
|
35
|
+
:metadata, # Hash - additional contextual data
|
|
36
|
+
keyword_init: true
|
|
37
|
+
) do
|
|
38
|
+
# Convert to hash, removing nil values
|
|
39
|
+
# @return [Hash]
|
|
40
|
+
def to_h
|
|
41
|
+
super.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if this is a specific event type
|
|
45
|
+
# @param event_name [Symbol] Event to check
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def event?(event_name)
|
|
48
|
+
event == event_name
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Organizations
|
|
4
|
+
# Centralized callback dispatch module.
|
|
5
|
+
# Handles executing callbacks with error isolation - callbacks should never
|
|
6
|
+
# break the main organization operations.
|
|
7
|
+
#
|
|
8
|
+
# @example Dispatching a callback
|
|
9
|
+
# Callbacks.dispatch(:organization_created, organization: org, user: user)
|
|
10
|
+
#
|
|
11
|
+
module Callbacks
|
|
12
|
+
# Supported callback events
|
|
13
|
+
EVENTS = %i[
|
|
14
|
+
organization_created
|
|
15
|
+
member_invited
|
|
16
|
+
member_joined
|
|
17
|
+
member_removed
|
|
18
|
+
role_changed
|
|
19
|
+
ownership_transferred
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# Dispatch a callback event.
|
|
25
|
+
# By default callbacks are isolated (errors logged, not raised).
|
|
26
|
+
# Pass strict: true when callback failures must abort the operation.
|
|
27
|
+
# @param event [Symbol] The event type (e.g., :organization_created)
|
|
28
|
+
# @param strict [Boolean] When true, callback errors are re-raised
|
|
29
|
+
# @param context_data [Hash] Data to pass to the callback via CallbackContext
|
|
30
|
+
def dispatch(event, strict: false, **context_data)
|
|
31
|
+
callback = callback_for(event)
|
|
32
|
+
return unless callback
|
|
33
|
+
|
|
34
|
+
context = CallbackContext.new(event: event, **context_data)
|
|
35
|
+
strict ? execute_strictly(callback, context) : execute_safely(event, callback, context)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the callback proc for an event
|
|
39
|
+
# @param event [Symbol] The event type
|
|
40
|
+
# @return [Proc, nil]
|
|
41
|
+
def callback_for(event)
|
|
42
|
+
config = Organizations.configuration
|
|
43
|
+
return nil unless config
|
|
44
|
+
|
|
45
|
+
case event
|
|
46
|
+
when :organization_created
|
|
47
|
+
config.on_organization_created_callback
|
|
48
|
+
when :member_invited
|
|
49
|
+
config.on_member_invited_callback
|
|
50
|
+
when :member_joined
|
|
51
|
+
config.on_member_joined_callback
|
|
52
|
+
when :member_removed
|
|
53
|
+
config.on_member_removed_callback
|
|
54
|
+
when :role_changed
|
|
55
|
+
config.on_role_changed_callback
|
|
56
|
+
when :ownership_transferred
|
|
57
|
+
config.on_ownership_transferred_callback
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Execute callback with error isolation
|
|
62
|
+
# @param event [Symbol] Event name (for logging)
|
|
63
|
+
# @param callback [Proc] The callback to execute
|
|
64
|
+
# @param context [CallbackContext] The context to pass
|
|
65
|
+
def execute_safely(event, callback, context)
|
|
66
|
+
return unless callback.respond_to?(:call)
|
|
67
|
+
|
|
68
|
+
invoke_callback(callback, context)
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
# Log but don't re-raise - callbacks should never break organization operations
|
|
71
|
+
log_error("[Organizations] Callback error for #{event}: #{e.class}: #{e.message}")
|
|
72
|
+
log_debug(e.backtrace&.join("\n"))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Execute callback and propagate any raised errors.
|
|
76
|
+
# Use this in flows where callbacks are expected to veto the operation.
|
|
77
|
+
def execute_strictly(callback, context)
|
|
78
|
+
return unless callback.respond_to?(:call)
|
|
79
|
+
|
|
80
|
+
invoke_callback(callback, context)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Call callback while supporting flexible callback arities.
|
|
84
|
+
def invoke_callback(callback, context)
|
|
85
|
+
case callback.arity
|
|
86
|
+
when 0
|
|
87
|
+
callback.call
|
|
88
|
+
when 1, -1, -2
|
|
89
|
+
callback.call(context)
|
|
90
|
+
else
|
|
91
|
+
callback.call(context)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Safe logging that works with or without Rails
|
|
96
|
+
def log_error(message)
|
|
97
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
98
|
+
Rails.logger.error(message)
|
|
99
|
+
else
|
|
100
|
+
warn(message)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def log_warn(message)
|
|
105
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
106
|
+
Rails.logger.warn(message)
|
|
107
|
+
else
|
|
108
|
+
warn(message)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def log_debug(message)
|
|
113
|
+
return unless message
|
|
114
|
+
|
|
115
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger&.debug?
|
|
116
|
+
Rails.logger.debug(message)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/numeric/time"
|
|
4
|
+
|
|
5
|
+
module Organizations
|
|
6
|
+
# Configuration class for the Organizations gem.
|
|
7
|
+
# Provides all customization options for organization behavior.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic configuration
|
|
10
|
+
# Organizations.configure do |config|
|
|
11
|
+
# config.always_create_personal_organization_for_each_user = true
|
|
12
|
+
# config.invitation_expiry = 7.days
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Custom roles
|
|
16
|
+
# Organizations.configure do |config|
|
|
17
|
+
# config.roles do
|
|
18
|
+
# role :viewer do
|
|
19
|
+
# can :view_organization
|
|
20
|
+
# end
|
|
21
|
+
# role :member, inherits: :viewer do
|
|
22
|
+
# can :create_resources
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
class Configuration
|
|
28
|
+
# === Authentication ===
|
|
29
|
+
# Method that returns the current user (default: :current_user)
|
|
30
|
+
attr_accessor :current_user_method
|
|
31
|
+
|
|
32
|
+
# Method that ensures user is authenticated (default: :authenticate_user!)
|
|
33
|
+
attr_accessor :authenticate_user_method
|
|
34
|
+
|
|
35
|
+
# === Auto-creation ===
|
|
36
|
+
# Create personal organization on user signup
|
|
37
|
+
attr_accessor :always_create_personal_organization_for_each_user
|
|
38
|
+
|
|
39
|
+
# Name for auto-created organizations
|
|
40
|
+
# Can be a String or a Proc/Lambda: ->(user) { "#{user.name}'s Workspace" }
|
|
41
|
+
attr_accessor :default_organization_name
|
|
42
|
+
|
|
43
|
+
# === Invitations ===
|
|
44
|
+
# How long invitations are valid
|
|
45
|
+
attr_accessor :invitation_expiry
|
|
46
|
+
|
|
47
|
+
# Default role for invited members
|
|
48
|
+
attr_accessor :default_invitation_role
|
|
49
|
+
|
|
50
|
+
# Custom mailer for invitations (class name as string)
|
|
51
|
+
attr_accessor :invitation_mailer
|
|
52
|
+
|
|
53
|
+
# === Limits ===
|
|
54
|
+
# Maximum organizations a user can own (nil = unlimited)
|
|
55
|
+
attr_accessor :max_organizations_per_user
|
|
56
|
+
|
|
57
|
+
# === Onboarding ===
|
|
58
|
+
# Require users to always belong to at least one organization
|
|
59
|
+
# Set to true to prevent users from leaving their last organization
|
|
60
|
+
attr_accessor :always_require_users_to_belong_to_one_organization
|
|
61
|
+
|
|
62
|
+
# === Session/Switching ===
|
|
63
|
+
# Session key for storing current organization ID
|
|
64
|
+
attr_accessor :session_key
|
|
65
|
+
|
|
66
|
+
# === Redirects ===
|
|
67
|
+
# Where to redirect when user has no organization
|
|
68
|
+
attr_accessor :redirect_path_when_no_organization
|
|
69
|
+
|
|
70
|
+
# === Engine configuration ===
|
|
71
|
+
attr_accessor :parent_controller
|
|
72
|
+
|
|
73
|
+
# === Handlers (blocks) ===
|
|
74
|
+
# @private - stored handler blocks
|
|
75
|
+
attr_reader :unauthorized_handler, :no_organization_handler
|
|
76
|
+
|
|
77
|
+
# === Callbacks ===
|
|
78
|
+
# @private - stored callback blocks
|
|
79
|
+
attr_reader :on_organization_created_callback,
|
|
80
|
+
:on_member_invited_callback,
|
|
81
|
+
:on_member_joined_callback,
|
|
82
|
+
:on_member_removed_callback,
|
|
83
|
+
:on_role_changed_callback,
|
|
84
|
+
:on_ownership_transferred_callback
|
|
85
|
+
|
|
86
|
+
# === Custom Roles ===
|
|
87
|
+
# @private - custom roles definition
|
|
88
|
+
attr_reader :custom_roles_definition
|
|
89
|
+
|
|
90
|
+
def initialize
|
|
91
|
+
# Authentication defaults
|
|
92
|
+
@current_user_method = :current_user
|
|
93
|
+
@authenticate_user_method = :authenticate_user!
|
|
94
|
+
|
|
95
|
+
# Auto-creation defaults
|
|
96
|
+
@always_create_personal_organization_for_each_user = false
|
|
97
|
+
@default_organization_name = "Personal"
|
|
98
|
+
|
|
99
|
+
# Invitation defaults
|
|
100
|
+
@invitation_expiry = 7.days
|
|
101
|
+
@default_invitation_role = :member
|
|
102
|
+
@invitation_mailer = "Organizations::InvitationMailer"
|
|
103
|
+
|
|
104
|
+
# Limits
|
|
105
|
+
@max_organizations_per_user = nil
|
|
106
|
+
|
|
107
|
+
# Onboarding
|
|
108
|
+
@always_require_users_to_belong_to_one_organization = false
|
|
109
|
+
|
|
110
|
+
# Session/switching
|
|
111
|
+
@session_key = :current_organization_id
|
|
112
|
+
|
|
113
|
+
# Redirects
|
|
114
|
+
@redirect_path_when_no_organization = "/organizations/new"
|
|
115
|
+
|
|
116
|
+
# Engine
|
|
117
|
+
@parent_controller = "::ApplicationController"
|
|
118
|
+
|
|
119
|
+
# Handlers (nil by default - use default behavior)
|
|
120
|
+
@unauthorized_handler = nil
|
|
121
|
+
@no_organization_handler = nil
|
|
122
|
+
|
|
123
|
+
# Callbacks (nil by default - no-op)
|
|
124
|
+
@on_organization_created_callback = nil
|
|
125
|
+
@on_member_invited_callback = nil
|
|
126
|
+
@on_member_joined_callback = nil
|
|
127
|
+
@on_member_removed_callback = nil
|
|
128
|
+
@on_role_changed_callback = nil
|
|
129
|
+
@on_ownership_transferred_callback = nil
|
|
130
|
+
|
|
131
|
+
# Custom roles
|
|
132
|
+
@custom_roles_definition = nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# === Handler Configuration Methods ===
|
|
136
|
+
|
|
137
|
+
# Configure unauthorized access handler
|
|
138
|
+
# @yield [context] Block to handle unauthorized access
|
|
139
|
+
# @yieldparam context [CallbackContext] Context with user, organization, permission info
|
|
140
|
+
#
|
|
141
|
+
# @example
|
|
142
|
+
# config.on_unauthorized do |context|
|
|
143
|
+
# redirect_to root_path, alert: "You don't have permission."
|
|
144
|
+
# end
|
|
145
|
+
#
|
|
146
|
+
def on_unauthorized(&block)
|
|
147
|
+
@unauthorized_handler = block if block_given?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Configure no organization handler
|
|
151
|
+
# @yield [context] Block to handle when user has no organization
|
|
152
|
+
# @yieldparam context [CallbackContext] Context with user info
|
|
153
|
+
#
|
|
154
|
+
# @example
|
|
155
|
+
# config.on_no_organization do |context|
|
|
156
|
+
# redirect_to new_organization_path, notice: "Please create an organization."
|
|
157
|
+
# end
|
|
158
|
+
#
|
|
159
|
+
def on_no_organization(&block)
|
|
160
|
+
@no_organization_handler = block if block_given?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# === Callback Configuration Methods ===
|
|
164
|
+
|
|
165
|
+
# Called when an organization is created
|
|
166
|
+
# @yield [context] Block to execute
|
|
167
|
+
# @yieldparam context [CallbackContext] Context with organization and user
|
|
168
|
+
def on_organization_created(&block)
|
|
169
|
+
@on_organization_created_callback = block if block_given?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Called when a member is invited
|
|
173
|
+
# @yield [context] Block to execute
|
|
174
|
+
# @yieldparam context [CallbackContext] Context with organization, invitation, invited_by
|
|
175
|
+
def on_member_invited(&block)
|
|
176
|
+
@on_member_invited_callback = block if block_given?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Called when a member joins (invitation accepted)
|
|
180
|
+
# @yield [context] Block to execute
|
|
181
|
+
# @yieldparam context [CallbackContext] Context with organization, membership, user
|
|
182
|
+
def on_member_joined(&block)
|
|
183
|
+
@on_member_joined_callback = block if block_given?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Called when a member is removed
|
|
187
|
+
# @yield [context] Block to execute
|
|
188
|
+
# @yieldparam context [CallbackContext] Context with organization, membership, user, removed_by
|
|
189
|
+
def on_member_removed(&block)
|
|
190
|
+
@on_member_removed_callback = block if block_given?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Called when a member's role changes
|
|
194
|
+
# @yield [context] Block to execute
|
|
195
|
+
# @yieldparam context [CallbackContext] Context with organization, membership, old_role, new_role, changed_by
|
|
196
|
+
def on_role_changed(&block)
|
|
197
|
+
@on_role_changed_callback = block if block_given?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Called when ownership is transferred
|
|
201
|
+
# @yield [context] Block to execute
|
|
202
|
+
# @yieldparam context [CallbackContext] Context with organization, old_owner, new_owner
|
|
203
|
+
def on_ownership_transferred(&block)
|
|
204
|
+
@on_ownership_transferred_callback = block if block_given?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# === Roles Configuration ===
|
|
208
|
+
|
|
209
|
+
# Define custom roles with permissions
|
|
210
|
+
# @yield DSL block for role definition
|
|
211
|
+
#
|
|
212
|
+
# @example
|
|
213
|
+
# config.roles do
|
|
214
|
+
# role :viewer do
|
|
215
|
+
# can :view_organization
|
|
216
|
+
# can :view_members
|
|
217
|
+
# end
|
|
218
|
+
# role :member, inherits: :viewer do
|
|
219
|
+
# can :create_resources
|
|
220
|
+
# end
|
|
221
|
+
# end
|
|
222
|
+
#
|
|
223
|
+
def roles(&block)
|
|
224
|
+
if block_given?
|
|
225
|
+
@custom_roles_definition = block
|
|
226
|
+
# Reset cached permissions so new roles take effect
|
|
227
|
+
Roles.reset!
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Resolve the default organization name for a user
|
|
232
|
+
# @param user [Object] The user object
|
|
233
|
+
# @return [String] The organization name
|
|
234
|
+
def resolve_default_organization_name(user)
|
|
235
|
+
case @default_organization_name
|
|
236
|
+
when Proc
|
|
237
|
+
@default_organization_name.call(user)
|
|
238
|
+
when String
|
|
239
|
+
@default_organization_name
|
|
240
|
+
else
|
|
241
|
+
"Personal"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Validate the configuration
|
|
246
|
+
# @raise [ConfigurationError] if configuration is invalid
|
|
247
|
+
def validate!
|
|
248
|
+
validate_authentication_methods!
|
|
249
|
+
validate_invitation_settings!
|
|
250
|
+
validate_limits!
|
|
251
|
+
true
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
def validate_authentication_methods!
|
|
257
|
+
unless @current_user_method.is_a?(Symbol)
|
|
258
|
+
raise ConfigurationError, "current_user_method must be a Symbol"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
unless @authenticate_user_method.is_a?(Symbol)
|
|
262
|
+
raise ConfigurationError, "authenticate_user_method must be a Symbol"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def validate_invitation_settings!
|
|
267
|
+
unless @invitation_expiry.nil? || @invitation_expiry.is_a?(ActiveSupport::Duration) || @invitation_expiry.is_a?(Numeric)
|
|
268
|
+
raise ConfigurationError, "invitation_expiry must be a Duration (e.g., 7.days) or nil"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
unless Roles::HIERARCHY.include?(@default_invitation_role.to_sym)
|
|
272
|
+
raise ConfigurationError, "default_invitation_role must be one of: #{Roles::HIERARCHY.join(', ')}"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def validate_limits!
|
|
277
|
+
if @max_organizations_per_user && !@max_organizations_per_user.is_a?(Integer)
|
|
278
|
+
raise ConfigurationError, "max_organizations_per_user must be an Integer or nil"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if @max_organizations_per_user && @max_organizations_per_user < 1
|
|
282
|
+
raise ConfigurationError, "max_organizations_per_user must be at least 1"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|