Skip to content

Conversation

@Germ-99
Copy link
Member

@Germ-99 Germ-99 commented Nov 20, 2025

Fixes: #146

Germ-99 and others added 2 commits November 20, 2025 05:54
* Update score validation to include subscore checks in statistics queue

* Update leaderboard record write to include metadata mapping

* Fix statistics storage with operator tags for leaderboard operations
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes "username already exists" errors when linking Discord accounts to game headsets by implementing conflict resolution logic. The solution detects when a username conflict occurs during authentication and attempts to recover by either linking the Discord ID to an existing unlinked account or identifying when the account is already properly linked.

Key changes:

  • Adds intelligent conflict detection and recovery for username collisions during account linking
  • Implements three recovery paths: already-linked accounts, unlinked accounts, and genuine conflicts
  • Introduces error type checking to distinguish AlreadyExists errors from other authentication failures

Comment on lines +100 to +122
// Link the account to the current discord user
logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"conflicting_user_id": conflictingUserID,
"username": username,
}).Info("Attempting to recover by linking custom_id to conflicting account")

if err := d.linkCustomID(ctx, logger, conflictingUserID, discordID); err != nil {
logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"conflicting_user_id": conflictingUserID,
"error": err,
}).Error("Failed to link custom ID during conflict recovery")
return "", fmt.Errorf("failed to recover account: %w", err)
}

logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"user_id": conflictingUserID,
"username": username,
}).Info("Recovery successful: custom_id linked to conflicting account")

return conflictingUserID, nil
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The automatic linking of custom ID to a conflicting account (lines 100-122) occurs without explicit user verification. This could be a security concern if:

  1. An attacker creates an account with a victim's Discord username before the victim links
  2. The victim then tries to link, and the system automatically associates their Discord ID with the attacker's account

Consider adding additional verification (e.g., checking account creation date, requiring explicit user confirmation, or validating that the account has minimal activity) before automatically linking to prevent potential account takeover scenarios.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discord usernames are unique

// Also check the error message string for the AlreadyExists code
// Good for cases where the error is wrapped or stringified
errMsg := err.Error()
return strings.Contains(errMsg, "code = AlreadyExists") || strings.Contains(errMsg, "AlreadyExists")
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string matching in isAlreadyExistsError checks for both "code = AlreadyExists" and "AlreadyExists" separately. The second check is redundant since any string containing "code = AlreadyExists" will also match "AlreadyExists". Consider simplifying to just check for "AlreadyExists" if the goal is to catch both formatted and unformatted error messages.

Suggested change
return strings.Contains(errMsg, "code = AlreadyExists") || strings.Contains(errMsg, "AlreadyExists")
return strings.Contains(errMsg, "AlreadyExists")

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +122
if conflictingCustomID != "" {
if conflictingCustomID == discordID {
// Return the existing user ID - they're already linked.
logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"user_id": conflictingUserID,
"username": username,
}).Info("Recovery successful: user already linked with matching custom_id")
return conflictingUserID, nil
}

// This is for cases where a different user owns the conflicting account.
// This cannot be automatically resolved.
logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"conflicting_user": conflictingCustomID,
"username": username,
}).Warn("Account conflict: different discord user owns the username")
return "", errors.New(
"Username is already linked to a different discord account. " +
"Reference: CONFLICT_DIFFERENT_OWNER",
)
}

// Link the account to the current discord user
logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"conflicting_user_id": conflictingUserID,
"username": username,
}).Info("Attempting to recover by linking custom_id to conflicting account")

if err := d.linkCustomID(ctx, logger, conflictingUserID, discordID); err != nil {
logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"conflicting_user_id": conflictingUserID,
"error": err,
}).Error("Failed to link custom ID during conflict recovery")
return "", fmt.Errorf("failed to recover account: %w", err)
}

logger.WithFields(map[string]interface{}{
"discord_id": discordID,
"user_id": conflictingUserID,
"username": username,
}).Info("Recovery successful: custom_id linked to conflicting account")

return conflictingUserID, nil
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition between checking the conflicting account's custom_id (line 76) and linking it (line 107). Another process could link a different discord ID to this account between these operations. Consider using a transaction or atomic operation if the underlying Nakama API supports it, or at least re-verify the custom_id is still empty after the link operation succeeds.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Bates <a@sprock.io>
Copilot AI added a commit that referenced this pull request Nov 20, 2025
- Simplify isAlreadyExistsError to remove redundant string check
- Add comprehensive security documentation for automatic linking
- Document race condition risks and mitigations
- Fix test compilation issue with operatorFromTag/operatorFromStatField

Co-authored-by: thesprockee <962164+thesprockee@users.noreply.github.com>
Signed-off-by: Andrew Bates <a@sprock.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle linking errors for "username already exists"

2 participants