Files
sure/app/controllers/registrations_controller.rb
Josh 97391a27ba Share family accounts with invited members on every sign-up path (#2650)
* fix: share family accounts with invited members on every sign-up path

A user who joined an existing family through an invitation was added to the
family but saw none of its accounts, even when the family's default sharing is
"share with all members". Only Invitation#accept_for created the AccountShare
records; the OIDC just-in-time sign-up, invite-token registration, and mobile
SSO onboarding paths all skipped it, so invitees landed in the family with an
empty account list. Signups routed into an invite-only default family had the
same gap.

Extract the sharing into Family#auto_share_existing_accounts_with, the single
entry point for "a member just joined, apply the family's sharing policy". It
honors default_account_sharing, only shares with a persisted member of the
family, grants read_only to guests and read_write to everyone else, excludes
accounts the user already owns, and is idempotent. accept_for and every
sign-up path call it, so no current or future join path can reintroduce the
empty-account bug or share accounts across families.

Also wrap the two SSO account-creation paths (OIDC JIT and mobile SSO) in a
transaction covering the user save, invitation acceptance, account sharing, and
identity creation, so a failure partway through can no longer leave a
half-onboarded user with no linked identity.

* address review: guest read_only symmetric in auto_share_with_family!, load-bearing guard comment, mobile SSO private-no-op test
2026-07-14 01:39:26 +02:00

117 lines
3.6 KiB
Ruby

class RegistrationsController < ApplicationController
skip_authentication
layout "auth"
before_action :ensure_signup_open, if: :self_hosted?
before_action :set_user, only: :create
before_action :set_invitation
before_action :validate_password_requirements, only: :create
def new
@user = User.new(email: @invitation&.email)
end
def create
if @invitation
@user.family = @invitation.family
@user.role = @invitation.role
@user.email = @invitation.email
elsif (default_family_id = Setting.invite_only_default_family_id).present? &&
Setting.onboarding_state == "invite_only" &&
(default_family = Family.find_by(id: default_family_id))
@user.family = default_family
@user.role = :member
else
family = Family.new
@user.family = family
@user.role = User.role_for_new_family_creator
end
if signup_with_invite_claim!
redirect_to root_path, notice: t(".success")
elsif @invite_code_invalid
redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code")
else
render :new, status: :unprocessable_entity, alert: t(".failure")
end
end
private
def set_invitation
token = params[:invitation]
token ||= params[:user][:invitation] if params[:user].present?
@invitation = Invitation.pending.find_by(token: token)
end
def set_user
@user = User.new user_params.except(:invite_code, :invitation)
end
def user_params(specific_param = nil)
params = self.params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code, :invitation)
specific_param ? params[specific_param] : params
end
# Keep save+claim atomic so failed signups never burn valid invite codes.
def signup_with_invite_claim!
invite_code = user_params[:invite_code]
@invite_code_invalid = invite_code_required? && invite_code.blank?
return false if @invite_code_invalid
success = false
ActiveRecord::Base.transaction do
unless @user.save
raise ActiveRecord::Rollback
end
if invite_code_required? && !InviteCode.claim!(invite_code)
@invite_code_invalid = true
raise ActiveRecord::Rollback
end
@invitation&.update!(accepted_at: Time.current)
# Joining an existing family must honor the family's default sharing
# policy so the user sees the accounts the family shares.
@user.family.auto_share_existing_accounts_with(@user)
@session = create_session_for(@user)
success = true
end
success
end
def validate_password_requirements
password = user_params[:password]
return if password.blank? # Let Rails built-in validations handle blank passwords
if password.length < 8
@user.errors.add(:password, "must be at least 8 characters")
end
unless password.match?(/[A-Z]/) && password.match?(/[a-z]/)
@user.errors.add(:password, "must include both uppercase and lowercase letters")
end
unless password.match?(/\d/)
@user.errors.add(:password, "must include at least one number")
end
unless password.match?(/[!@#$%^&*(),.?":{}|<>]/)
@user.errors.add(:password, "must include at least one special character")
end
if @user.errors.present?
render :new, status: :unprocessable_entity
end
end
def ensure_signup_open
return unless Setting.onboarding_state == "closed"
redirect_to new_session_path, alert: t("registrations.closed")
end
end