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:
Ilia Denisov
2026-07-15 00:34:58 +02:00
parent 48614e622d
commit da25eac070
9 changed files with 127 additions and 27 deletions
+6
View File
@@ -285,6 +285,12 @@ func (s *Server) handleLinkVKMerge(c *gin.Context) {
// or a completed link (the active account's refreshed profile).
func (s *Server) confirmResultResponse(c *gin.Context, activeID uuid.UUID, res link.ConfirmResult) linkResultResponse {
ctx := c.Request.Context()
if res.Merged {
// A guest initiator's merge ran inline (the guest-primary rule; no confirmation step),
// so render the completed merge — the client adopts the switched session and lands on
// the surviving durable account as if it simply logged in.
return s.mergeResultResponse(c, res.Merge)
}
if res.MergeRequired {
out := linkResultResponse{Status: "merge_required", SecondaryUserID: res.SecondaryID.String()}
if acc, err := s.accounts.GetByID(ctx, res.SecondaryID); err == nil {