mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 15:59:02 +00:00
* fix(family-sharing): prevent silent data loss when rehoming or removing users Fixes #1689. Two destructive paths could strand a pre-existing user's family and accounts: 1. Invitation#accept_for unconditionally overwrote user.family_id, orphaning the prior family + its accounts with no user able to reach them. 2. Settings::ProfilesController#destroy then called @user.destroy when an admin removed the rehomed member, destroying the only login path back to the now-orphaned data. Add hard-block guards on both paths. accept_for refuses when the invitee already belongs to a family with accounts; ProfilesController#destroy refuses when the member owns accounts in another family (legacy state from the old flow). InvitationsController#create surfaces a specific, actionable flash so the admin understands why the auto-accept was refused. No automatic recovery of already-orphaned data — that needs a separate one-shot script per dosubot's analysis on the issue. * fix(family-sharing): scope invite orphan-guard to invitee-owned accounts (#1896 review) Codex flagged (P1) and the maintainer review independently raised that would_orphan_existing_family? keyed off user.family.accounts.exists? — any account in the invitee's current family — which wrongly blocked a non-owner member from leaving a multi-user household. Rename to would_orphan_owned_accounts? and key off user.owned_accounts.where.not(family_id: family_id), making the invite guard symmetric with the destroy-path guard in Settings::ProfilesController. A member who owns no accounts now orphans nothing by moving and is free to accept the invitation; an owner is still blocked. Add a regression test for the non-owner case and update the existing tests to give the invitee explicit account ownership. * Remove extra comments per project conventions --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com>
71 lines
1.8 KiB
Ruby
71 lines
1.8 KiB
Ruby
class InvitationsController < ApplicationController
|
|
skip_authentication only: :accept
|
|
def new
|
|
@invitation = Invitation.new
|
|
end
|
|
|
|
def create
|
|
unless Current.user.admin?
|
|
flash[:alert] = t(".failure")
|
|
redirect_to settings_profile_path
|
|
return
|
|
end
|
|
|
|
@invitation = Current.family.invitations.build(invitation_params)
|
|
@invitation.inviter = Current.user
|
|
|
|
if @invitation.save
|
|
normalized_email = @invitation.email.to_s.strip.downcase
|
|
existing_user = User.find_by(email: normalized_email)
|
|
if existing_user && @invitation.would_orphan_owned_accounts?(existing_user)
|
|
flash[:alert] = t(".existing_user_has_family_data")
|
|
elsif existing_user && @invitation.accept_for(existing_user)
|
|
flash[:notice] = t(".existing_user_added")
|
|
elsif existing_user
|
|
flash[:alert] = t(".failure")
|
|
else
|
|
InvitationMailer.invite_email(@invitation).deliver_later unless self_hosted?
|
|
flash[:notice] = t(".success")
|
|
end
|
|
else
|
|
flash[:alert] = t(".failure")
|
|
end
|
|
|
|
redirect_to settings_profile_path
|
|
end
|
|
|
|
def accept
|
|
@invitation = Invitation.find_by!(token: params[:id])
|
|
|
|
if @invitation.pending?
|
|
render :accept_choice, layout: "auth"
|
|
else
|
|
raise ActiveRecord::RecordNotFound
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
unless Current.user.admin?
|
|
flash[:alert] = t("invitations.destroy.not_authorized")
|
|
redirect_to settings_profile_path
|
|
return
|
|
end
|
|
|
|
@invitation = Current.family.invitations.find(params[:id])
|
|
|
|
if @invitation.destroy
|
|
flash[:notice] = t("invitations.destroy.success")
|
|
else
|
|
flash[:alert] = t("invitations.destroy.failure")
|
|
end
|
|
|
|
redirect_to settings_profile_path
|
|
end
|
|
|
|
private
|
|
|
|
def invitation_params
|
|
params.require(:invitation).permit(:email, :role)
|
|
end
|
|
end
|