mirror of
https://github.com/we-promise/sure.git
synced 2026-07-20 08:45:22 +00:00
* 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
117 lines
3.6 KiB
Ruby
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
|