feat(link): auto-merge a guest initiator into its durable account (seamless sign-in)
When a guest links an identity (email/TG/VK) that already belongs to a durable account, the guest-primary rule already made the durable account the survivor and switched the session — but the client still showed an irreversible 'merge two accounts?' confirmation, which is nonsense from the user's side (they are simply signing into their account). The confirm step now merges inline for a GUEST initiator and returns the completed merge (the switched token); a durable initiator still gets the explicit confirmation (consolidating two real accounts is consequential). The active-game guard still refuses, surfaced as the clear error.merge_active_game_conflict message rather than swallowed. Also fixes the merged-away device-local games: their human/host seat was recorded under the retired account id, so after the switch the lobby and game header could not identify 'me' and showed every seat as an opponent (the game still played — turn logic is seat-index based). applyLinkResult now re-points local game seats from the retired id to the survivor (repointLocalGameSeats). Docs: FUNCTIONAL.md (+_ru).
This commit is contained in:
@@ -31,13 +31,39 @@ func NewService(emails *account.EmailService, accounts *account.Store, merger *a
|
||||
return &Service{emails: emails, accounts: accounts, merger: merger, sessions: sessions}
|
||||
}
|
||||
|
||||
// ConfirmResult reports the outcome of a confirm step. Exactly one of Linked or
|
||||
// MergeRequired is set; SecondaryID is the account to be retired when a merge is
|
||||
// required (the caller renders an irreversible-merge confirmation from it).
|
||||
// ConfirmResult reports the outcome of a confirm step. Exactly one of Linked,
|
||||
// MergeRequired or Merged is set. SecondaryID is the account to be retired when a merge
|
||||
// is required (the caller renders an irreversible-merge confirmation from it). Merged is
|
||||
// set when the caller was a guest, so the merge ran inline (see confirmOrAutoMerge) and
|
||||
// Merge carries its result (the switched-session token for the surviving durable account).
|
||||
type ConfirmResult struct {
|
||||
Linked bool
|
||||
MergeRequired bool
|
||||
SecondaryID uuid.UUID
|
||||
Merged bool
|
||||
Merge MergeResult
|
||||
}
|
||||
|
||||
// confirmOrAutoMerge decides a required merge at the confirm step. A durable caller gets
|
||||
// the explicit MergeRequired confirmation (consolidating two real accounts is irreversible
|
||||
// and consequential — the user must see it). A GUEST caller does not: by the guest-primary
|
||||
// rule the durable other account survives and the ephemeral guest is retired, folding its
|
||||
// games/wallet/stats in — from the user's side it is simply "logged into my account", so the
|
||||
// merge runs inline and the client just adopts the switched session. The active-game guard can
|
||||
// still refuse (accountmerge.ErrActiveGameConflict), surfaced to the caller unchanged.
|
||||
func (s *Service) confirmOrAutoMerge(ctx context.Context, callerID, owner uuid.UUID) (ConfirmResult, error) {
|
||||
caller, err := s.accounts.GetByID(ctx, callerID)
|
||||
if err != nil {
|
||||
return ConfirmResult{}, err
|
||||
}
|
||||
if !caller.IsGuest {
|
||||
return ConfirmResult{MergeRequired: true, SecondaryID: owner}, nil
|
||||
}
|
||||
res, err := s.merge(ctx, callerID, owner)
|
||||
if err != nil {
|
||||
return ConfirmResult{}, err
|
||||
}
|
||||
return ConfirmResult{Merged: true, Merge: res}, nil
|
||||
}
|
||||
|
||||
// MergeResult reports a completed merge. PrimaryID is the surviving account.
|
||||
@@ -67,7 +93,7 @@ func (s *Service) ConfirmEmail(ctx context.Context, accountID uuid.UUID, email,
|
||||
}
|
||||
return ConfirmResult{Linked: true}, nil
|
||||
}
|
||||
return ConfirmResult{MergeRequired: true, SecondaryID: owner}, nil
|
||||
return s.confirmOrAutoMerge(ctx, accountID, owner)
|
||||
}
|
||||
|
||||
// MergeEmail re-verifies the code and merges the address's account into the
|
||||
@@ -103,7 +129,7 @@ func (s *Service) ConfirmTelegram(ctx context.Context, callerID uuid.UUID, exter
|
||||
if owner == callerID {
|
||||
return ConfirmResult{Linked: true}, nil
|
||||
}
|
||||
return ConfirmResult{MergeRequired: true, SecondaryID: owner}, nil
|
||||
return s.confirmOrAutoMerge(ctx, callerID, owner)
|
||||
}
|
||||
|
||||
// MergeTelegram merges the account owning a gateway-validated Telegram identity
|
||||
@@ -150,7 +176,7 @@ func (s *Service) ConfirmVK(ctx context.Context, callerID uuid.UUID, externalID
|
||||
if owner == callerID {
|
||||
return ConfirmResult{Linked: true}, nil
|
||||
}
|
||||
return ConfirmResult{MergeRequired: true, SecondaryID: owner}, nil
|
||||
return s.confirmOrAutoMerge(ctx, callerID, owner)
|
||||
}
|
||||
|
||||
// MergeVK merges the account owning a gateway-validated VK identity into the caller's
|
||||
|
||||
Reference in New Issue
Block a user