Module: Organizations::ControllerHelpers

Extended by:
ActiveSupport::Concern
Includes:
CurrentUserResolution
Included in:
ApplicationController, PublicInvitationsController
Defined in:
lib/organizations/controller_helpers.rb

Overview

Controller helpers to be included in host application controllers. Provides current_organization context, session-based switching, and permission guards.

Examples:

Include in ApplicationController

class ApplicationController < ActionController::Base
  include Organizations::ControllerHelpers
end

Using guards

class ProjectsController < ApplicationController
  before_action :require_organization!
  before_action :require_organization_admin!, only: [:create, :destroy]
end

Instance Method Summary collapse

Instance Method Details

#accept_pending_organization_invitation!(user, token: nil, switch: true, skip_email_validation: false, return_failure: false) ⇒ Organizations::InvitationAcceptanceResult, ...

Accept a pending organization invitation for a user This is the canonical method for handling invitation acceptance after signup/signin.

Examples:

Basic usage in after_sign_in_path_for

def (resource)
  if (result = accept_pending_organization_invitation!(resource))
    return redirect_path_after_invitation_accepted(result.invitation, user: resource)
  end
  super
end

Parameters:

  • user (User)

    The user accepting the invitation

  • token (String, nil) (defaults to: nil)

    Explicit token (uses session token if not provided)

  • switch (Boolean) (defaults to: true)

    Whether to switch to the organization after acceptance (default: true)

  • skip_email_validation (Boolean) (defaults to: false)

    Skip email matching check (default: false)

  • return_failure (Boolean) (defaults to: false)

    Return a structured failure object instead of nil

Returns:



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/organizations/controller_helpers.rb', line 229

def accept_pending_organization_invitation!(
  user,
  token: nil,
  switch: true,
  skip_email_validation: false,
  return_failure: false
)
  return invitation_acceptance_failure(:missing_user, return_failure: return_failure) unless user

  # Track whether we're using an explicit token that differs from session
  # Only skip session clearing if explicit token fails and differs from session
  explicit_token = token.presence
  session_token = pending_organization_invitation_token
  invitation_token = explicit_token || session_token
  return invitation_acceptance_failure(:missing_token, return_failure: return_failure) unless invitation_token

  # When explicit token differs from session, don't clear session on failure
  using_different_explicit_token = explicit_token && explicit_token != session_token

  invitation = Organizations::Invitation.find_by(token: invitation_token)
  unless invitation
    # Only clear session if we were using the session token (or same token)
    clear_pending_organization_invitation! unless using_different_explicit_token
    return invitation_acceptance_failure(:invitation_not_found, return_failure: return_failure)
  end

  if invitation.expired?
    # Only clear session if we were using the session token (or same token)
    clear_pending_organization_invitation! unless using_different_explicit_token
    return invitation_acceptance_failure(
      :invitation_expired,
      return_failure: return_failure,
      invitation: invitation
    )
  end

  # Check email match (unless skipping validation)
  unless skip_email_validation
    if user.respond_to?(:email) && !invitation.for_email?(user.email)
      # Email mismatch - keep token intact to allow switching accounts
      return invitation_acceptance_failure(
        :email_mismatch,
        return_failure: return_failure,
        invitation: invitation
      )
    end
  end

  status = :accepted
  membership = nil

  begin
    membership = invitation.accept!(user, skip_email_validation: skip_email_validation)
  rescue Organizations::InvitationExpired
    # Race condition: invitation expired between our check and accept!
    clear_pending_organization_invitation! unless using_different_explicit_token
    return invitation_acceptance_failure(
      :invitation_expired,
      return_failure: return_failure,
      invitation: invitation
    )
  rescue Organizations::InvitationAlreadyAccepted
    # Check if user is actually a member
    membership = Organizations::Membership.find_by(
      user_id: user.id,
      organization_id: invitation.organization_id
    )
    unless membership
      # Data integrity anomaly: invitation marked accepted but membership missing
      if defined?(Rails) && Rails.respond_to?(:logger)
        Rails.logger.warn "[Organizations] InvitationAlreadyAccepted raised but no membership found for user=#{user.id} org=#{invitation.organization_id}"
      end
      clear_pending_organization_invitation! unless using_different_explicit_token
      return invitation_acceptance_failure(
        :already_accepted_without_membership,
        return_failure: return_failure,
        invitation: invitation
      )
    end
    status = :already_member
  end

  # Attempt to switch to the organization
  switched = true
  if switch
    begin
      switch_to_organization!(invitation.organization, user: user)
    rescue Organizations::NotAMember
      switched = false
    end
  else
    switched = false
  end

  # Always clear session token on successful acceptance
  clear_pending_organization_invitation!

  Organizations::InvitationAcceptanceResult.new(
    status: status,
    invitation: invitation,
    membership: membership,
    switched: switched
  )
end

#clear_pending_organization_invitation!nil

Clear pending invitation token and memoized values

Returns:

  • (nil)


143
144
145
146
147
148
# File 'lib/organizations/controller_helpers.rb', line 143

def clear_pending_organization_invitation!
  session.delete(pending_invitation_session_key)
  remove_instance_variable(:@_pending_organization_invitation) if defined?(@_pending_organization_invitation)
  remove_instance_variable(:@_pending_organization_invitation_token) if defined?(@_pending_organization_invitation_token)
  nil
end

#create_organization_and_switch!(user, attributes = {}) ⇒ Organizations::Organization Also known as: create_organization_with_context!

Creates an organization and switches context in one call.

Parameters:

  • user (User)

    The user who will own the created organization

  • attributes (Hash) (defaults to: {})

    Attributes passed to create_organization!

Returns:



433
434
435
436
437
# File 'lib/organizations/controller_helpers.rb', line 433

def create_organization_and_switch!(user, attributes = {})
  organization = user.create_organization!(attributes)
  switch_to_organization!(organization, user: user)
  organization
end

#current_membershipOrganizations::Membership?

Returns the current user's membership in the current organization

Returns:



78
79
80
81
82
83
84
85
# File 'lib/organizations/controller_helpers.rb', line 78

def current_membership
  return @_current_membership if defined?(@_current_membership)

  user = organizations_current_user
  return @_current_membership = nil unless user && current_organization

  @_current_membership = user.memberships.find_by(organization_id: current_organization.id)
end

#current_organizationOrganizations::Organization?

Returns the current organization from session Validates membership - if user was removed, auto-switches to next available org Falls back to most recently joined org if no session set Memoized within the request

Returns:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/organizations/controller_helpers.rb', line 43

def current_organization
  return @_current_organization if defined?(@_current_organization)

  user = organizations_current_user
  return @_current_organization = nil unless user

  session_key = Organizations.configuration.session_key
  org_id = session[session_key]

  # Find organization AND verify membership
  # Use is_member_of? which has DB fallback for stale loaded associations
  org = org_id ? Organizations::Organization.find_by(id: org_id) : nil

  if org && user.is_member_of?(org)
    # Valid membership - use this org
    user._current_organization_id = org.id
    @_current_organization = org
  else
    # User was removed from this org OR no session set
    # Auto-switch to next available org (most recently joined)
    clear_organization_session!

    fallback_org = fallback_organization_for(user)
    if fallback_org
      session[session_key] = fallback_org.id
      user._current_organization_id = fallback_org.id
      @_current_organization = fallback_org
    else
      @_current_organization = nil
    end
  end
end

#current_organization=(org) ⇒ Object

Sets the current organization in session

Parameters:



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/organizations/controller_helpers.rb', line 446

def current_organization=(org)
  session_key = Organizations.configuration.session_key

  if org
    session[session_key] = org.id
    @_current_organization = org
    @_current_membership = nil # Clear cached membership

    # Update user's context
    user = organizations_current_user
    user._current_organization_id = org.id if user
  else
    clear_organization_session!
  end
end

#handle_pending_invitation_acceptance_for(user, redirect: false, token: nil, switch: true, skip_email_validation: false, notice: true) ⇒ String, ...

Accept pending invitation and either return redirect path or perform redirect.

Parameters:

  • user (User)

    The user accepting the invitation

  • redirect (Boolean) (defaults to: false)

    When true, performs redirect_to and returns true/false

  • token (String, nil) (defaults to: nil)

    Explicit invitation token (optional)

  • switch (Boolean) (defaults to: true)

    Whether to switch organization context

  • skip_email_validation (Boolean) (defaults to: false)

    Whether to skip invitation email checks

  • notice (Boolean, String, Proc) (defaults to: true)

    Flash notice behavior (default: true)

Returns:

  • (String, Boolean, nil)


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/organizations/controller_helpers.rb', line 187

def handle_pending_invitation_acceptance_for(
  user,
  redirect: false,
  token: nil,
  switch: true,
  skip_email_validation: false,
  notice: true
)
  path = pending_invitation_acceptance_redirect_path_for(
    user,
    token: token,
    switch: switch,
    skip_email_validation: skip_email_validation,
    notice: notice
  )

  return false if redirect && path.nil?
  return nil unless path
  return path unless redirect

  redirect_to path
  true
end

#no_organization_redirect_path(user: nil) ⇒ String

Alias used in many host apps for readability.

Parameters:

  • user (User, nil) (defaults to: nil)

    Optional user context for Proc redirects

Returns:

  • (String)


405
406
407
# File 'lib/organizations/controller_helpers.rb', line 405

def no_organization_redirect_path(user: nil)
  redirect_path_when_no_organization(user: user)
end

#organization_signed_in?Boolean

Check if there's an active organization

Returns:

  • (Boolean)


89
90
91
# File 'lib/organizations/controller_helpers.rb', line 89

def organization_signed_in?
  current_organization.present?
end

#pending_invitation_acceptance_redirect_path_for(user, token: nil, switch: true, skip_email_validation: false, notice: true) ⇒ String?

Accept pending invitation (if present) and return post-accept redirect path. Returns nil when there is no pending/acceptable invitation.

Parameters:

  • user (User)

    The user accepting the invitation

  • token (String, nil) (defaults to: nil)

    Explicit invitation token (optional)

  • switch (Boolean) (defaults to: true)

    Whether to switch organization context

  • skip_email_validation (Boolean) (defaults to: false)

    Whether to skip invitation email checks

  • notice (Boolean, String, Proc) (defaults to: true)

    Flash notice behavior (default: true)

Returns:

  • (String, nil)

    Redirect path or nil



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/organizations/controller_helpers.rb', line 159

def pending_invitation_acceptance_redirect_path_for(
  user,
  token: nil,
  switch: true,
  skip_email_validation: false,
  notice: true
)
  result = accept_pending_organization_invitation!(
    user,
    token: token,
    switch: switch,
    skip_email_validation: skip_email_validation
  )
  return nil unless result

  set_pending_invitation_acceptance_notice!(result, user: user, notice: notice)
  redirect_path_after_invitation_accepted(result.invitation, user: user)
end

#pending_organization_invitationOrganizations::Invitation?

Returns the pending invitation if token is valid and invitation is usable Clears token if invitation is missing, expired, or already accepted

Returns:



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/organizations/controller_helpers.rb', line 104

def pending_organization_invitation
  token = pending_organization_invitation_token
  return nil unless token

  # Check memoized value (keyed by token to handle mid-request changes)
  if defined?(@_pending_organization_invitation_token) && @_pending_organization_invitation_token == token
    return @_pending_organization_invitation
  end

  invitation = Organizations::Invitation.find_by(token: token)

  unless invitation
    clear_pending_organization_invitation!
    return nil
  end

  if invitation.expired? || invitation.accepted?
    clear_pending_organization_invitation!
    return nil
  end

  @_pending_organization_invitation_token = token
  @_pending_organization_invitation = invitation
end

#pending_organization_invitation?Boolean

Check if there's a valid pending invitation

Returns:

  • (Boolean)


131
132
133
# File 'lib/organizations/controller_helpers.rb', line 131

def pending_organization_invitation?
  pending_organization_invitation.present?
end

#pending_organization_invitation_emailString?

Returns the email from the pending invitation, if present

Returns:

  • (String, nil)


137
138
139
# File 'lib/organizations/controller_helpers.rb', line 137

def pending_organization_invitation_email
  pending_organization_invitation&.email
end

#pending_organization_invitation_tokenString?

Returns the pending invitation token from session

Returns:

  • (String, nil)


97
98
99
# File 'lib/organizations/controller_helpers.rb', line 97

def pending_organization_invitation_token
  session[pending_invitation_session_key]
end

#redirect_path_after_invitation_accepted(invitation, user: nil) ⇒ String

Returns the path to redirect to after invitation is accepted Uses configured value or falls back to root path

Parameters:

  • invitation (Organizations::Invitation)

    The invitation that was accepted

  • user (User, nil) (defaults to: nil)

    The user who accepted (optional)

Returns:

  • (String)

    The redirect path



357
358
359
360
361
362
363
364
365
366
# File 'lib/organizations/controller_helpers.rb', line 357

def redirect_path_after_invitation_accepted(invitation, user: nil)
  config_value = Organizations.configuration.redirect_path_after_invitation_accepted

  resolve_controller_redirect_path(
    config_value,
    invitation,
    user,
    default: -> { default_after_accept_redirect_path }
  )
end

#redirect_path_after_organization_switched(organization, user: nil) ⇒ String

Returns the path to redirect to after organization switch Uses configured value or falls back to root path

Parameters:

  • organization (Organizations::Organization)

    The organization switched to

  • user (User, nil) (defaults to: nil)

    The user who switched (optional)

Returns:

  • (String)

    The redirect path



374
375
376
377
378
379
380
381
382
383
# File 'lib/organizations/controller_helpers.rb', line 374

def redirect_path_after_organization_switched(organization, user: nil)
  config_value = Organizations.configuration.redirect_path_after_organization_switched

  resolve_controller_redirect_path(
    config_value,
    organization,
    user,
    default: -> { default_after_switch_redirect_path }
  )
end

#redirect_path_when_invitation_requires_authentication(invitation = nil, user: nil) ⇒ String

Returns the path to redirect to when an invitation requires authentication Uses configured value or falls back to registration/root path

Parameters:

  • invitation (Organizations::Invitation, nil) (defaults to: nil)

    The invitation (optional)

  • user (User, nil) (defaults to: nil)

    The user (optional)

Returns:

  • (String)

    The redirect path



340
341
342
343
344
345
346
347
348
349
# File 'lib/organizations/controller_helpers.rb', line 340

def redirect_path_when_invitation_requires_authentication(invitation = nil, user: nil)
  config_value = Organizations.configuration.redirect_path_when_invitation_requires_authentication

  resolve_controller_redirect_path(
    config_value,
    invitation,
    user,
    default: -> { default_auth_required_redirect_path(invitation) }
  )
end

#redirect_path_when_no_organization(user: nil) ⇒ String

Returns the redirect path used when the user has no active organization. Uses configured value or falls back to /organizations/new.

Parameters:

  • user (User, nil) (defaults to: nil)

    Optional user context for Proc redirects

Returns:

  • (String)


390
391
392
393
394
395
396
397
398
399
# File 'lib/organizations/controller_helpers.rb', line 390

def redirect_path_when_no_organization(user: nil)
  config_value = Organizations.configuration.redirect_path_when_no_organization
  redirect_user = user || organizations_current_user

  resolve_controller_redirect_path(
    config_value,
    redirect_user,
    default: -> { "/organizations/new" }
  )
end

#redirect_to_no_organization!(alert: nil, notice: nil) ⇒ false

Redirect helper for no-organization flows.

When both alert and notice are nil, uses the default alert message.

Parameters:

  • alert (String, nil) (defaults to: nil)

    Flash alert message

  • notice (String, nil) (defaults to: nil)

    Flash notice message

Returns:

  • (false)


416
417
418
419
420
421
422
423
424
425
426
# File 'lib/organizations/controller_helpers.rb', line 416

def redirect_to_no_organization!(alert: nil, notice: nil)
  flash_options = {}
  flash_options[:alert] = alert unless alert.nil?
  flash_options[:notice] = notice unless notice.nil?

  # Keep current behavior for existing apps when nothing is configured/passed.
  flash_options[:alert] = Organizations.t(:"notices.select_or_create_organization") if flash_options.empty?

  redirect_to no_organization_redirect_path, **flash_options
  false
end

#require_organization!Object

Requires a current organization to be set

Examples:

before_action :require_organization!


494
495
496
497
498
# File 'lib/organizations/controller_helpers.rb', line 494

def require_organization!
  return if current_organization

  handle_no_organization
end

#require_organization_admin!Object

Requires the user to be an admin (or owner) of the current organization Convenience method for require_organization_role!(:admin)

Examples:

before_action :require_organization_admin!, only: [:edit, :update]


535
536
537
# File 'lib/organizations/controller_helpers.rb', line 535

def require_organization_admin!
  require_organization_role!(:admin)
end

#require_organization_owner!Object

Requires the user to be the owner of the current organization Convenience method for require_organization_role!(:owner)

Examples:

before_action :require_organization_owner!, only: [:destroy]


543
544
545
# File 'lib/organizations/controller_helpers.rb', line 543

def require_organization_owner!
  require_organization_role!(:owner)
end

#require_organization_permission_to!(permission) ⇒ Object

Requires the user to have a specific permission

Examples:

before_action -> { require_organization_permission_to!(:invite_members) }

Parameters:

  • permission (Symbol)

    The permission to check



521
522
523
524
525
526
527
528
529
# File 'lib/organizations/controller_helpers.rb', line 521

def require_organization_permission_to!(permission)
  require_organization!
  return unless current_organization

  user = organizations_current_user
  return if user&.has_organization_permission_to?(permission)

  handle_unauthorized(permission: permission)
end

#require_organization_role!(role) ⇒ Object

Requires the user to have at least the specified role

Examples:

before_action -> { require_organization_role!(:admin) }, only: [:edit]

Parameters:

  • role (Symbol)

    The minimum required role



504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/organizations/controller_helpers.rb', line 504

def require_organization_role!(role)
  require_organization!
  return unless current_organization

  user = organizations_current_user
  return if user&.is_at_least?(role, in: current_organization)

  handle_unauthorized(
    permission: role,
    required_role: role
  )
end

#switch_to_organization!(org, user: nil) ⇒ Object

Switches to a different organization

Parameters:

  • org (Organizations::Organization)
  • user (User, nil) (defaults to: nil)

    Explicit user to switch for (useful in auth-transition flows)

Raises:



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/organizations/controller_helpers.rb', line 466

def switch_to_organization!(org, user: nil)
  acting_user = user || organizations_current_user(refresh: true)

  unless membership_exists_for?(acting_user, org)
    raise Organizations::NotAMember.new(
      Organizations.t(:"errors.not_a_member"),
      organization: org,
      user: acting_user
    )
  end

  self.current_organization = org
  # current_organization= calls organizations_current_user (without refresh) and
  # updates that user's _current_organization_id. But in auth-transition flows:
  # 1. The memoized user may still be nil (sign-in just happened)
  # 2. An explicit user: was passed that differs from the memoized user
  # In either case, acting_user won't be updated by current_organization=.
  # This explicit assignment ensures acting_user always gets the correct org ID.
  acting_user._current_organization_id = org.id if acting_user.respond_to?(:_current_organization_id=)
  mark_membership_as_recent!(acting_user, org)
end