Skip to content

Fix/unique displayname v2#384

Open
compiler041 wants to merge 2 commits into
AOSSIE-Org:mainfrom
compiler041:fix/unique-displayname-v2
Open

Fix/unique displayname v2#384
compiler041 wants to merge 2 commits into
AOSSIE-Org:mainfrom
compiler041:fix/unique-displayname-v2

Conversation

@compiler041

@compiler041 compiler041 commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Addressed Issues:
Fixes : #382

Enforces uniqueness of displayName (which serves as the username in DebateAI)
across all three layers: database index, backend validation, and frontend UI.

Changes
Backend
db/db.go — Added unique sparse MongoDB index on displayName field
controllers/auth.go — Added duplicate displayName check during signup
(both email/password and Google OAuth flows)
controllers/profile_controller.go — Added duplicate displayName check
during profile update; added CheckDisplayName availability endpoint;
removed separate username field (it was redundant with displayName)
routes/profile.go — Added CheckDisplayNameRouteHandler
cmd/server/main.go — Registered /user/check-displayname route
models/user.go — Removed redundant Username field
Frontend
services/profileService.ts — Added checkDisplayNameAvailability function;
removed username param from updateProfile (now uses displayName only)
Pages/Profile.tsx — Display name edit field now shows real-time availability
check; profile data refetches after successful save so UI updates immediately
without reload
Testing
Verified two users cannot sign up with the same display name
Verified updating display name to an existing name returns 409
Verified UI shows "Display name already taken" feedback in real time
Verified profile updates immediately after save without page reload
Screenshots/Recordings:
##BEFORE:
before

##AFTER:

Screenshot 2026-04-16 122058

AI Usage Disclosure:
We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact. AI slop is strongly discouraged and may lead to banning and blocking. Do not spam our repos with AI slop.

##Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • I have filled this PR template completely and carefully, and I understand that my PR may be closed without review otherwise.

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces display name uniqueness validation across the application. A MongoDB index on the displayName field ensures uniqueness, with corresponding backend checks during user signup and Google login, frontend availability validation with debouncing, and updated profile save logic that prevents duplicate display names while refetching data on successful updates.

Changes

Cohort / File(s) Summary
Database & Model Setup
backend/db/db.go, backend/models/user.go
Added EnsureIndexes() function creating a unique, sparse MongoDB index on the displayName field; removed explanatory comments from both files without changing runtime behavior.
Authentication Logic
backend/controllers/auth.go
Added display name uniqueness checks in both GoogleLogin and SignUp handlers, returning HTTP 400 when a display name is already taken; pre-validates before user creation and handles duplicate key errors.
Profile Controller & Routing
backend/controllers/profile_controller.go, backend/routes/profile.go
Enhanced UpdateProfile with explicit display name conflict detection before update; added new CheckDisplayName handler and corresponding route handler to validate availability via query parameter.
Server Configuration
backend/cmd/server/main.go
Integrated db.EnsureIndexes() call immediately after MongoDB connection to ensure indexes exist before application startup; registered new GET /user/check-displayname authenticated route.
Frontend Profile Service & Types
frontend/src/services/profileService.ts, frontend/src/types/user.ts
Added checkDisplayNameAvailability() service function to query display name availability; improved error handling in updateProfile with response parsing.
Frontend Profile UI
frontend/src/Pages/Profile.tsx
Integrated debounced display name availability checking with UI feedback (checking/available/taken states); blocks save when display name is taken; refetches profile and related data after successful update.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Frontend)
    participant Frontend as Profile.tsx
    participant Service as profileService
    participant API as Backend API
    participant DB as MongoDB

    rect rgba(100, 150, 200, 0.5)
        note over User,DB: Display Name Availability Check Flow
        User->>Frontend: Input display name
        Frontend->>Frontend: Debounce (300ms)
        Frontend->>Service: checkDisplayNameAvailability()
        Service->>API: GET /user/check-displayname?displayName=...
        API->>DB: Query users collection
        DB-->>API: Return query result
        API-->>Service: {available: true/false}
        Service-->>Frontend: Response received
        Frontend->>Frontend: Update usernameStatus state
        Frontend->>User: Show availability feedback
    end

    rect rgba(150, 100, 200, 0.5)
        note over User,DB: Profile Update with Display Name Validation
        User->>Frontend: Click Save button
        Frontend->>Frontend: Validate usernameStatus != "taken"
        Frontend->>Service: updateProfile(displayName, ...)
        Service->>API: PUT /profile with displayName
        API->>DB: Query for existing displayName
        DB-->>API: Check if taken by another user
        alt Display name available
            API->>DB: Update user document with $set
            DB-->>API: Update successful
            API-->>Service: {success: true}
            Service-->>Frontend: Update response
            Frontend->>API: GET /profile (refetch)
            API->>DB: Fetch updated profile
            DB-->>API: Return profile
            API-->>Frontend: Profile data
            Frontend->>Frontend: Refresh profile, dashboard, recentDebates
            Frontend->>User: Show success
        else Display name taken
            API-->>Service: Error 409 Conflict
            Service-->>Frontend: Throw error
            Frontend->>User: Show error message
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • bhavik-mangla

Poem

🐰 Hop, hop, the names must be unique and true,\
A MongoDB index keeps duplicates at bay (who knew?),\
From login to signup, each display name checked with care,\
Debounce and validate, availability's fair!\
*~ Celebrating your uniqueness in every compare~ ✨*

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title refers to a real and significant part of the changeset (enforcing unique displayName), but it is vague and generic with 'Fix/unique displayname v2' lacking clarity about the scope and primary purpose. Consider a more descriptive title that clearly explains the main change, e.g., 'Enforce unique displayName across signup, profile update, and database' or 'Add displayName uniqueness validation and availability check'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/Pages/Profile.tsx (1)

258-300: ⚠️ Potential issue | 🟡 Minor

handleSubmit allows saving an empty or whitespace-only displayName.

The guard on line 262 only blocks usernameStatus === "taken". If the user clears the input, the onChange handler (line 709-712) sets usernameStatus back to "idle" and leaves dashboard.profile.displayName empty — at which point Save is re-enabled and submits "". Given the new unique+sparse index on displayName (in backend/db/db.go) doesn't actually exclude empty strings, this can cause a backend 409/500 for the second user who tries it, and is a poor UX either way.

Also consider blocking submit while usernameStatus === "checking" so a keyboard-Enter during the debounce window doesn't bypass the availability check.

🔧 Suggested fix
 if (field === "displayName" && usernameStatus === "taken") {
   setErrorMessage("Display name is already taken.");
   return;
 }
+if (
+  field === "displayName" &&
+  (!dashboard.profile.displayName?.trim() || usernameStatus === "checking")
+) {
+  setErrorMessage(
+    usernameStatus === "checking"
+      ? "Please wait while we verify the display name."
+      : "Display name cannot be empty."
+  );
+  return;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/Profile.tsx` around lines 258 - 300, In handleSubmit, add
validation to reject empty or whitespace-only displayName and prevent submission
while usernameStatus is "checking"; specifically, before calling updateProfile
in the handleSubmit function, trim dashboard.profile.displayName and if it is
empty setErrorMessage("Display name cannot be empty.") and return, and also if
usernameStatus === "checking" setErrorMessage("Checking display name
availability, please wait.") and return; keep the existing usernameStatus ===
"taken" check, then proceed to call updateProfile/getProfile and the existing
success/error handlers as before.
backend/controllers/auth.go (1)

73-109: ⚠️ Potential issue | 🟠 Major

Race window: GoogleLogin relies on pre-check but doesn't translate duplicate-key errors from InsertOne.

The pre-check at lines 75-83 is not atomic with InsertOne on line 102, so two concurrent Google logins picking the same nickname can both pass the check and race into InsertOne. The unique index on displayName (added in backend/db/db.go) will reject one of them, but this handler returns a generic 500 ("Failed to create user") instead of a 400 "Display name already taken" — inconsistent with SignUp (which handles this at lines 196-199).

🔧 Suggested fix
 result, err := db.MongoDatabase.Collection("users").InsertOne(dbCtx, newUser)
 if err != nil {
+    if mongo.IsDuplicateKeyError(err) {
+        ctx.JSON(400, gin.H{"error": "Display name already taken"})
+        return
+    }
     ctx.JSON(500, gin.H{"error": "Failed to create user", "message": err.Error()})
     return
 }

Additionally, for Google OAuth you may want to auto-suffix nickname on collision (e.g. nickname-2, nickname-3) rather than failing the login outright, since the user has no way to edit the name during the OAuth flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/auth.go` around lines 73 - 109, The GoogleLogin handler
currently does a non-atomic pre-check for displayName then calls InsertOne, but
InsertOne can fail with a duplicate-key error under race and is treated as a
generic 500; update the GoogleLogin flow (the code around InsertOne in the
GoogleLogin handler) to detect Mongo duplicate-key errors (e.g. inspect the
returned error for mongo.WriteException with Code 11000 or use a helper like
mongo.IsDuplicateKeyError if available) and translate that into the same 400
"Display name already taken" response as SignUp does; optionally implement a
small retry/suffix strategy (generate nickname-2, nickname-3, attempt InsertOne
again) before returning the 400 so OAuth users can be auto-suffixed instead of
failing.
backend/controllers/profile_controller.go (1)

300-349: ⚠️ Potential issue | 🟠 Major

Partial updates and empty displayName both cause data loss or conflicts.

Two issues in the profile update path:

  1. Partial-update vulnerability. The updateProfile function accepts optional parameters (twitter?, instagram?, linkedin?, avatarUrl?). When omitted, JSON.stringify excludes them from the payload. The backend's plain string struct fields deserialize missing JSON properties to "", and setFields unconditionally $sets every field to these empty defaults. While the current frontend always passes all profile fields, this code is vulnerable to accidental data loss if frontend behavior changes or if another client does a partial update. Either require all fields in every request (and document that contract), or switch to *string and only include keys that were present in the payload.

  2. Empty displayName violates unique sparse index. The displayName index is correctly configured as unique and sparse. However, a sparse index excludes missing fields but still indexes empty strings. When newDisplayName == "", the uniqueness pre-check is skipped (correct), but setFields["displayName"] = "" is still persisted. The second user who clears their displayName will have displayName: "" in the database, triggering mongo.IsDuplicateKeyError with a misleading "Display name already taken" 409 response. Either skip the field when empty (so the sparse index ignores it) or use $unset:

Proposed fix (sketch)
 	var updateData struct {
-		DisplayName string `json:"displayName"`
-		Bio         string `json:"bio"`
-		Twitter     string `json:"twitter"`
-		Instagram   string `json:"instagram"`
-		LinkedIn    string `json:"linkedin"`
-		AvatarURL   string `json:"avatarUrl"`
+		DisplayName *string `json:"displayName"`
+		Bio         *string `json:"bio"`
+		Twitter     *string `json:"twitter"`
+		Instagram   *string `json:"instagram"`
+		LinkedIn    *string `json:"linkedin"`
+		AvatarURL   *string `json:"avatarUrl"`
 	}
 	...
 	setFields := bson.M{"updatedAt": time.Now()}
 	unsetFields := bson.M{}
 	if updateData.DisplayName != nil {
 		trimmed := strings.TrimSpace(*updateData.DisplayName)
 		if trimmed == "" {
 			unsetFields["displayName"] = ""
 		} else {
 			// uniqueness pre-check against `trimmed` ...
 			setFields["displayName"] = trimmed
 		}
 	}
 	// repeat for other fields ...
 	update := bson.M{"$set": setFields}
 	if len(unsetFields) > 0 {
 		update["$unset"] = unsetFields
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/profile_controller.go` around lines 300 - 349, Change the
inline updateData struct fields to pointer types (e.g., DisplayName *string, Bio
*string, Twitter *string, Instagram *string, LinkedIn *string, AvatarURL
*string) so you can detect omitted keys; only add keys to the bson.M "$set" when
the corresponding pointer is non-nil and non-empty after trimming. For
displayName: run the uniqueness pre-check only when updateData.DisplayName !=
nil and trimmed != ""; if trimmed == "" treat it as a clear-request and add that
field to a "$unset" map instead of setting it to "", or simply omit it to
preserve sparseness. Finally call UpdateOne with a combined update document
(include "$set" only if non-empty and "$unset" only if non-empty) and keep the
mongo.IsDuplicateKeyError handling as-is.
🧹 Nitpick comments (1)
backend/controllers/profile_controller.go (1)

353-382: Consider case-insensitive displayName comparison.

The displayName unique index (backend/db/db.go) lacks collation for case-insensitive matching. Users typically expect usernames to be case-insensitive; "Alice" and "alice" should be treated as duplicates. Add a collation-based unique index with {locale: "en", strength: 2} and update queries in CheckDisplayName and auth signup flow to match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/profile_controller.go` around lines 353 - 382, The
displayName uniqueness must be enforced case-insensitively: update the users
collection index creation in backend/db/db.go to create a unique index on
"displayName" with a Collation set to {Locale: "en", Strength: 2} (ensure you
drop/recreate or alter the index if needed), and update all reads/writes that
match displayName to use the same collation; specifically modify
CheckDisplayName (the MongoDatabase.Collection("users").FindOne call in
CheckDisplayName) to pass a FindOneOptions with Collation {Locale: "en",
Strength: 2} and change the signup flow (the signup handler that inserts/queries
users) to use the same collation when checking for existing displayName and when
creating the unique index so comparisons treat "Alice" and "alice" as the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/cmd/server/main.go`:
- Around line 36-38: Indentation in main is mixed spaces/tabs around the
db.EnsureIndexes() call and a trailing tab after the handler, causing
gofmt/govet failures; open the main function and replace the space-indented
lines around db.EnsureIndexes() with tab indentation to match the rest of the
file (the block containing db.EnsureIndexes()), remove any trailing tab
characters after the HTTP handler registration, and run gofmt (or gofmt -w) to
ensure consistent tabs throughout.

In `@backend/controllers/auth.go`:
- Around line 154-164: The signup flow derives defaultDisplayName via
utils.ExtractNameFromEmail and currently rejects signups when that displayName
collides (existingDisplayName) which blocks users from resolving it; update the
logic in the SignUp handler around db.MongoDatabase.Collection("users").FindOne
so that instead of returning "Display name already taken" it tries to
auto-resolve collisions by attempting incremental suffixes (e.g.,
defaultDisplayName, defaultDisplayName-2, defaultDisplayName-3, ...) and
checking existence until an unused displayName is found, then continue creating
the user with that resolved displayName; alternatively implement a deterministic
fallback that assigns a unique temporary displayName (e.g., "user-<random>")
when all suffix attempts fail and persist that value so the account is created
rather than returning a 400.

In `@backend/controllers/profile_controller.go`:
- Around line 316-325: The pre-check using
db.MongoDatabase.Collection("users").FindOne when validating newDisplayName
currently swallows non-ErrNoDocuments errors; change the logic in the display
name uniqueness block (the newDisplayName/FindOne/decode/existing flow) to
explicitly handle mongo.ErrNoDocuments as the only acceptable “not found” case
and return an internal server error (HTTP 500) for any other err returned by
FindOne (and log the DB error), following the pattern used in auth.go
(SignUp/GoogleLogin); keep the existing behavior of returning HTTP 409 when err
== nil and existing.Email != email.
- Around line 145-156: The UpdateOne call that sets user.Rating and
user.LastRatingUpdate is currently discarding errors (using "_, _ ="), so
capture the result and error from db.MongoDatabase.Collection("users").UpdateOne
(using the existing dbCtx and user.ID), check if err != nil, and log the error
with context (include user.ID, user.Rating, and operation name) via the
package's logger rather than ignoring it; after logging decide whether to return
an error response from the GetProfile handler or continue with the in-memory
defaults, but do not silently swallow the UpdateOne error.
- Line 24: The exported sentinel ErrUsernameTaken is unused; either remove its
declaration or wire it into the relevant error paths: in UpdateProfile and
CheckDisplayName return ErrUsernameTaken when a username/display name conflict
is detected (replace current string/error returns), and update any callers to
compare errors against ErrUsernameTaken (errors.Is or direct equality) so the
sentinel is honored; if you choose removal, delete the ErrUsernameTaken variable
and any references in comments/tests to avoid dead export.

In `@backend/db/db.go`:
- Around line 58-75: EnsureIndexes currently creates a unique sparse index on
users.displayName which doesn't prevent duplicates of empty strings and can
crash startup if duplicates exist; update the index options in the EnsureIndexes
function (the indexModel used with MongoDatabase.Collection("users") /
usersCol.Indexes().CreateOne) to use SetPartialFilterExpression that enforces
uniqueness only for non-empty string displayName values (e.g. filter where
displayName exists, is a string, and $ne ""), and change the startup error
handling where EnsureIndexes is invoked (avoid log.Fatalf on error in main.go)
to log the failure and continue so operators can remediate duplicates without
bringing the server down.

In `@frontend/src/Pages/Profile.tsx`:
- Around line 706-725: The onChange debounce uses debounceTimer.current but
never clears it on unmount and doesn't guard against stale/Out‑of‑order
responses; add a useEffect cleanup that clears debounceTimer.current on unmount,
and change the async debounce handler to use an AbortController (passed into
checkDisplayNameAvailability) or maintain a latestQuery ref (e.g.,
latestDisplayNameRef) to verify the response matches the most recent val before
calling setUsernameStatus; update calls to getAuthToken,
checkDisplayNameAvailability, and setUsernameStatus accordingly and ensure any
thrown abort is handled so state is not set for cancelled/stale requests.

---

Outside diff comments:
In `@backend/controllers/auth.go`:
- Around line 73-109: The GoogleLogin handler currently does a non-atomic
pre-check for displayName then calls InsertOne, but InsertOne can fail with a
duplicate-key error under race and is treated as a generic 500; update the
GoogleLogin flow (the code around InsertOne in the GoogleLogin handler) to
detect Mongo duplicate-key errors (e.g. inspect the returned error for
mongo.WriteException with Code 11000 or use a helper like
mongo.IsDuplicateKeyError if available) and translate that into the same 400
"Display name already taken" response as SignUp does; optionally implement a
small retry/suffix strategy (generate nickname-2, nickname-3, attempt InsertOne
again) before returning the 400 so OAuth users can be auto-suffixed instead of
failing.

In `@backend/controllers/profile_controller.go`:
- Around line 300-349: Change the inline updateData struct fields to pointer
types (e.g., DisplayName *string, Bio *string, Twitter *string, Instagram
*string, LinkedIn *string, AvatarURL *string) so you can detect omitted keys;
only add keys to the bson.M "$set" when the corresponding pointer is non-nil and
non-empty after trimming. For displayName: run the uniqueness pre-check only
when updateData.DisplayName != nil and trimmed != ""; if trimmed == "" treat it
as a clear-request and add that field to a "$unset" map instead of setting it to
"", or simply omit it to preserve sparseness. Finally call UpdateOne with a
combined update document (include "$set" only if non-empty and "$unset" only if
non-empty) and keep the mongo.IsDuplicateKeyError handling as-is.

In `@frontend/src/Pages/Profile.tsx`:
- Around line 258-300: In handleSubmit, add validation to reject empty or
whitespace-only displayName and prevent submission while usernameStatus is
"checking"; specifically, before calling updateProfile in the handleSubmit
function, trim dashboard.profile.displayName and if it is empty
setErrorMessage("Display name cannot be empty.") and return, and also if
usernameStatus === "checking" setErrorMessage("Checking display name
availability, please wait.") and return; keep the existing usernameStatus ===
"taken" check, then proceed to call updateProfile/getProfile and the existing
success/error handlers as before.

---

Nitpick comments:
In `@backend/controllers/profile_controller.go`:
- Around line 353-382: The displayName uniqueness must be enforced
case-insensitively: update the users collection index creation in
backend/db/db.go to create a unique index on "displayName" with a Collation set
to {Locale: "en", Strength: 2} (ensure you drop/recreate or alter the index if
needed), and update all reads/writes that match displayName to use the same
collation; specifically modify CheckDisplayName (the
MongoDatabase.Collection("users").FindOne call in CheckDisplayName) to pass a
FindOneOptions with Collation {Locale: "en", Strength: 2} and change the signup
flow (the signup handler that inserts/queries users) to use the same collation
when checking for existing displayName and when creating the unique index so
comparisons treat "Alice" and "alice" as the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be0bc5ea-575f-42f6-b16c-37d5226b0bcd

📥 Commits

Reviewing files that changed from the base of the PR and between 09ef1bb and cbd19e7.

📒 Files selected for processing (9)
  • backend/cmd/server/main.go
  • backend/controllers/auth.go
  • backend/controllers/profile_controller.go
  • backend/db/db.go
  • backend/models/user.go
  • backend/routes/profile.go
  • frontend/src/Pages/Profile.tsx
  • frontend/src/services/profileService.ts
  • frontend/src/types/user.ts

Comment on lines +36 to +38
if err := db.EnsureIndexes(); err != nil {
log.Fatalf("Failed to ensure indexes: %v", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

gofmt: mixed tab/space indentation.

Lines 37-38 are indented with spaces while the rest of main uses tabs; same on line 108 (and a trailing tab after the handler). This will fail gofmt/go vet in CI.

🔧 Fix
-	if err := db.EnsureIndexes(); err != nil {
-    log.Fatalf("Failed to ensure indexes: %v", err)
-    }
+	if err := db.EnsureIndexes(); err != nil {
+		log.Fatalf("Failed to ensure indexes: %v", err)
+	}
-        auth.GET("/user/check-displayname", routes.CheckDisplayNameRouteHandler)		
+		auth.GET("/user/check-displayname", routes.CheckDisplayNameRouteHandler)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := db.EnsureIndexes(); err != nil {
log.Fatalf("Failed to ensure indexes: %v", err)
}
if err := db.EnsureIndexes(); err != nil {
log.Fatalf("Failed to ensure indexes: %v", err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/cmd/server/main.go` around lines 36 - 38, Indentation in main is
mixed spaces/tabs around the db.EnsureIndexes() call and a trailing tab after
the handler, causing gofmt/govet failures; open the main function and replace
the space-indented lines around db.EnsureIndexes() with tab indentation to match
the rest of the file (the block containing db.EnsureIndexes()), remove any
trailing tab characters after the HTTP handler registration, and run gofmt (or
gofmt -w) to ensure consistent tabs throughout.

Comment on lines +154 to +164
// Check if displayName is already taken
defaultDisplayName := utils.ExtractNameFromEmail(request.Email)
var existingDisplayName models.User
err = db.MongoDatabase.Collection("users").FindOne(dbCtx, bson.M{"displayName": defaultDisplayName}).Decode(&existingDisplayName)
if err == nil {
ctx.JSON(400, gin.H{"error": "Display name already taken"})
return
} else if !errors.Is(err, mongo.ErrNoDocuments) {
ctx.JSON(500, gin.H{"error": "Database error"})
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

UX: SignUp rejects with "Display name already taken" but the user cannot choose/change the display name at signup.

defaultDisplayName is deterministically derived from the email local part. If a collision occurs (e.g., john@a.com already exists and john@b.com signs up), the signup fails with an error the user has no way to resolve — they can't supply an alternate display name. Consider one of:

  • Auto-suffix on collision (e.g., try john, john-2, john-3, …) before returning the generic email-already-exists error, or
  • Allow the signup to complete with a temporary/unique displayName (e.g., user-<random>) and force the user to choose a name on first login.

Either approach avoids a dead-end for users whose email local parts happen to collide.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/auth.go` around lines 154 - 164, The signup flow derives
defaultDisplayName via utils.ExtractNameFromEmail and currently rejects signups
when that displayName collides (existingDisplayName) which blocks users from
resolving it; update the logic in the SignUp handler around
db.MongoDatabase.Collection("users").FindOne so that instead of returning
"Display name already taken" it tries to auto-resolve collisions by attempting
incremental suffixes (e.g., defaultDisplayName, defaultDisplayName-2,
defaultDisplayName-3, ...) and checking existence until an unused displayName is
found, then continue creating the user with that resolved displayName;
alternatively implement a deterministic fallback that assigns a unique temporary
displayName (e.g., "user-<random>") when all suffix attempts fail and persist
that value so the account is created rather than returning a 400.

)

// Calculate new Elo ratings using float64
var ErrUsernameTaken = errors.New("username already taken")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '\bErrUsernameTaken\b' --type=go -C2

Repository: AOSSIE-Org/DebateAI

Length of output: 452


Remove unused ErrUsernameTaken sentinel or wire it into error handling paths.

The variable declared at line 24 is never referenced or returned anywhere in the codebase. Either integrate it into UpdateProfile and CheckDisplayName error responses, or remove it to avoid dead exports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/profile_controller.go` at line 24, The exported sentinel
ErrUsernameTaken is unused; either remove its declaration or wire it into the
relevant error paths: in UpdateProfile and CheckDisplayName return
ErrUsernameTaken when a username/display name conflict is detected (replace
current string/error returns), and update any callers to compare errors against
ErrUsernameTaken (errors.Is or direct equality) so the sentinel is honored; if
you choose removal, delete the ErrUsernameTaken variable and any references in
comments/tests to avoid dead export.

Comment on lines 145 to 156
if user.Rating == 0 {
user.Rating = 1200.0
user.LastRatingUpdate = time.Now()
_, err = db.MongoDatabase.Collection("users").UpdateOne(
_, _ = db.MongoDatabase.Collection("users").UpdateOne(
dbCtx,
bson.M{"_id": user.ID},
bson.M{"$set": bson.M{
"rating": user.Rating,
"lastRatingUpdate": user.LastRatingUpdate,
}},
)
if err != nil {
} else {
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent failure on rating default-update.

The previous error check was dropped in favor of _, _ =. If this UpdateOne fails transiently, user.Rating == 0 persists in DB and the branch will fire again on every subsequent GetProfile call, compounding load. At minimum log the error so it's observable.

Proposed fix
-		_, _ = db.MongoDatabase.Collection("users").UpdateOne(
+		if _, uerr := db.MongoDatabase.Collection("users").UpdateOne(
 			dbCtx,
 			bson.M{"_id": user.ID},
 			bson.M{"$set": bson.M{
 				"rating":           user.Rating,
 				"lastRatingUpdate": user.LastRatingUpdate,
 			}},
-		)
+		); uerr != nil {
+			log.Printf("GetProfile: failed to persist default rating for %s: %v", user.Email, uerr)
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if user.Rating == 0 {
user.Rating = 1200.0
user.LastRatingUpdate = time.Now()
_, err = db.MongoDatabase.Collection("users").UpdateOne(
_, _ = db.MongoDatabase.Collection("users").UpdateOne(
dbCtx,
bson.M{"_id": user.ID},
bson.M{"$set": bson.M{
"rating": user.Rating,
"lastRatingUpdate": user.LastRatingUpdate,
}},
)
if err != nil {
} else {
}
}
if user.Rating == 0 {
user.Rating = 1200.0
user.LastRatingUpdate = time.Now()
if _, uerr := db.MongoDatabase.Collection("users").UpdateOne(
dbCtx,
bson.M{"_id": user.ID},
bson.M{"$set": bson.M{
"rating": user.Rating,
"lastRatingUpdate": user.LastRatingUpdate,
}},
); uerr != nil {
log.Printf("GetProfile: failed to persist default rating for %s: %v", user.Email, uerr)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/profile_controller.go` around lines 145 - 156, The
UpdateOne call that sets user.Rating and user.LastRatingUpdate is currently
discarding errors (using "_, _ ="), so capture the result and error from
db.MongoDatabase.Collection("users").UpdateOne (using the existing dbCtx and
user.ID), check if err != nil, and log the error with context (include user.ID,
user.Rating, and operation name) via the package's logger rather than ignoring
it; after logging decide whether to return an error response from the GetProfile
handler or continue with the in-memory defaults, but do not silently swallow the
UpdateOne error.

Comment on lines +316 to +325
// Check displayName uniqueness
newDisplayName := strings.TrimSpace(updateData.DisplayName)
if newDisplayName != "" {
var existing models.User
err := db.MongoDatabase.Collection("users").FindOne(dbCtx, bson.M{"displayName": newDisplayName}).Decode(&existing)
if err == nil && existing.Email != email {
ctx.JSON(http.StatusConflict, gin.H{"error": "Display name already taken"})
return
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-ErrNoDocuments errors from the pre-check are silently swallowed.

If FindOne returns a transient DB error, execution falls through to the UpdateOne instead of returning 500. Mirror the pattern used in auth.go (SignUp / GoogleLogin) so the handler behaves consistently:

Proposed fix
 	if newDisplayName != "" {
 		var existing models.User
 		err := db.MongoDatabase.Collection("users").FindOne(dbCtx, bson.M{"displayName": newDisplayName}).Decode(&existing)
-		if err == nil && existing.Email != email {
-			ctx.JSON(http.StatusConflict, gin.H{"error": "Display name already taken"})
-			return
+		if err == nil {
+			if existing.Email != email {
+				ctx.JSON(http.StatusConflict, gin.H{"error": "Display name already taken"})
+				return
+			}
+		} else if !errors.Is(err, mongo.ErrNoDocuments) {
+			ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
+			return
 		}
 	}

The unique index will still protect correctness, but the current control flow makes a DB outage indistinguishable from a successful "not taken" check.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check displayName uniqueness
newDisplayName := strings.TrimSpace(updateData.DisplayName)
if newDisplayName != "" {
var existing models.User
err := db.MongoDatabase.Collection("users").FindOne(dbCtx, bson.M{"displayName": newDisplayName}).Decode(&existing)
if err == nil && existing.Email != email {
ctx.JSON(http.StatusConflict, gin.H{"error": "Display name already taken"})
return
}
}
// Check displayName uniqueness
newDisplayName := strings.TrimSpace(updateData.DisplayName)
if newDisplayName != "" {
var existing models.User
err := db.MongoDatabase.Collection("users").FindOne(dbCtx, bson.M{"displayName": newDisplayName}).Decode(&existing)
if err == nil {
if existing.Email != email {
ctx.JSON(http.StatusConflict, gin.H{"error": "Display name already taken"})
return
}
} else if !errors.Is(err, mongo.ErrNoDocuments) {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/profile_controller.go` around lines 316 - 325, The
pre-check using db.MongoDatabase.Collection("users").FindOne when validating
newDisplayName currently swallows non-ErrNoDocuments errors; change the logic in
the display name uniqueness block (the newDisplayName/FindOne/decode/existing
flow) to explicitly handle mongo.ErrNoDocuments as the only acceptable “not
found” case and return an internal server error (HTTP 500) for any other err
returned by FindOne (and log the DB error), following the pattern used in
auth.go (SignUp/GoogleLogin); keep the existing behavior of returning HTTP 409
when err == nil and existing.Email != email.

Comment thread backend/db/db.go
Comment on lines +58 to +75
func EnsureIndexes() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

usersCol := MongoDatabase.Collection("users")

indexModel := mongo.IndexModel{
Keys: bson.D{{Key: "displayName", Value: 1}},
Options: options.Index().SetUnique(true).SetSparse(true).SetName("unique_displayName"),
}

_, err := usersCol.Indexes().CreateOne(ctx, indexModel)
if err != nil {
return fmt.Errorf("failed to create displayName index: %w", err)
}
log.Println("DisplayName unique index ensured")
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether any users currently have empty or missing displayName that would collide under the sparse+unique index.
rg -nP --type=go 'DisplayName\s+string\s+`bson:"displayName[^`]*"' 
rg -nP --type=go -C2 'ExtractNameFromEmail'

Repository: AOSSIE-Org/DebateAI

Length of output: 2573


🏁 Script executed:

# 1. Get the full User struct definition
cat -n backend/models/user.go | head -50

# 2. Check how displayName is assigned during user creation
rg -A10 "DisplayName\s*=" backend/controllers/auth.go | head -40

# 3. Check main.go for how EnsureIndexes is called and error handling
rg -B5 -A5 "EnsureIndexes" backend/main.go

Repository: AOSSIE-Org/DebateAI

Length of output: 2233


🏁 Script executed:

# Find main.go or entry point
find . -name "main.go" -type f

# Check how displayName is set during user creation
rg -B3 -A3 "DisplayName.*=" backend/controllers/auth.go | head -50

# Look for where EnsureIndexes is called
rg "EnsureIndexes" . --type go

Repository: AOSSIE-Org/DebateAI

Length of output: 572


🏁 Script executed:

# Check error handling of EnsureIndexes in server main.go
rg -B5 -A5 "EnsureIndexes" ./backend/cmd/server/main.go

# Check complete user creation flow to see if displayName can stay empty
rg -B10 -A10 "DisplayName.*=" ./backend/controllers/auth.go | head -80

Repository: AOSSIE-Org/DebateAI

Length of output: 1231


🏁 Script executed:

# Check the complete user creation/insertion code
rg -A30 "hashedPassword.*bcrypt" ./backend/controllers/auth.go

# Check if displayName is explicitly set or required before insertion
rg -B5 -A15 "InsertOne.*user" ./backend/controllers/auth.go

Repository: AOSSIE-Org/DebateAI

Length of output: 1845


🏁 Script executed:

# Check for migrations or db initialization scripts
find . -name "*migrat*" -o -name "*seed*" | grep -v node_modules

# Check for other places where users are created or displayName is modified
rg "displayName.*:" . --type go | grep -v test | head -30

# Verify ExtractNameFromEmail never returns empty
cat -n ./backend/utils/auth.go | sed -n '43,50p'

Repository: AOSSIE-Org/DebateAI

Length of output: 2960


🏁 Script executed:

# Check profile controller for displayName update validation
rg -B5 -A15 "displayName.*newDisplayName" ./backend/controllers/profile_controller.go

# Check if there's any validation on displayName input
rg "newDisplayName" ./backend/controllers/profile_controller.go -B10 -A5

Repository: AOSSIE-Org/DebateAI

Length of output: 2171


Sparse index won't prevent duplicate empty displayNames; startup crash risk on existing duplicates.

Two confirmed issues with the index strategy:

  1. Sparse index does not handle empty strings. The SetSparse(true) option only excludes documents where the displayName field is missing, not where it's an empty string "". However, the User model stores DisplayName string with bson:"displayName" (no omitempty), and the profile update endpoint allows users to set displayName to an empty string after trimming:

    newDisplayName := strings.TrimSpace(updateData.DisplayName)
    // ...
    "displayName": newDisplayName  // can be ""

    Two users with empty displayName will violate the unique constraint, either during this initial CreateOne call (if legacy data exists) or during subsequent updates.

  2. Server crashes on startup if any duplicates exist. The EnsureIndexes() call uses log.Fatalf on failure in main.go, which terminates the server. Any pre-existing or corrupted duplicate displayName values (including empty strings) will cause a hard startup failure, making deployments fragile.

Suggested mitigations:

  • Use a PartialFilterExpression to only enforce uniqueness on non-empty values:
    Options: options.Index().
        SetUnique(true).
        SetName("unique_displayName").
        SetPartialFilterExpression(bson.M{
            "displayName": bson.M{"$exists": true, "$type": "string", "$ne": ""},
        }),
  • Change index creation to log and continue rather than fatal, allowing manual remediation of conflicting documents before re-deployment.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/db/db.go` around lines 58 - 75, EnsureIndexes currently creates a
unique sparse index on users.displayName which doesn't prevent duplicates of
empty strings and can crash startup if duplicates exist; update the index
options in the EnsureIndexes function (the indexModel used with
MongoDatabase.Collection("users") / usersCol.Indexes().CreateOne) to use
SetPartialFilterExpression that enforces uniqueness only for non-empty string
displayName values (e.g. filter where displayName exists, is a string, and $ne
""), and change the startup error handling where EnsureIndexes is invoked (avoid
log.Fatalf on error in main.go) to log the failure and continue so operators can
remediate duplicates without bringing the server down.

Comment on lines +706 to +725
onChange={(e) => {
const val = e.target.value;
setDashboard({ ...dashboard, profile: { ...profile, displayName: val } });
if (!val.trim()) {
setUsernameStatus("idle");
return;
}
if (debounceTimer.current) clearTimeout(debounceTimer.current);
setUsernameStatus("checking");
debounceTimer.current = setTimeout(async () => {
try {
const token = getAuthToken();
if (!token) return;
const res = await checkDisplayNameAvailability(token, val.trim());
setUsernameStatus(res.available ? "available" : "taken");
} catch {
setUsernameStatus("idle");
}
}, 300);
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Debounce timer is never cleared on unmount; can cause state updates on unmounted component and stale results.

debounceTimer.current is set inside onChange but there is no useEffect cleanup that clears it when the component unmounts. If the user navigates away during the 300ms window, checkDisplayNameAvailability will still fire and setUsernameStatus will be called on an unmounted component. There's also no request-cancellation (e.g., AbortController), so overlapping edits can resolve out of order and overwrite each other's results.

🔧 Suggested fix
+useEffect(() => {
+  return () => {
+    if (debounceTimer.current) clearTimeout(debounceTimer.current);
+  };
+}, []);

Consider also tracking the latest query (e.g., compare the typed value to a ref) before calling setUsernameStatus, or use AbortController in checkDisplayNameAvailability to drop stale responses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/Profile.tsx` around lines 706 - 725, The onChange debounce
uses debounceTimer.current but never clears it on unmount and doesn't guard
against stale/Out‑of‑order responses; add a useEffect cleanup that clears
debounceTimer.current on unmount, and change the async debounce handler to use
an AbortController (passed into checkDisplayNameAvailability) or maintain a
latestQuery ref (e.g., latestDisplayNameRef) to verify the response matches the
most recent val before calling setUsernameStatus; update calls to getAuthToken,
checkDisplayNameAvailability, and setUsernameStatus accordingly and ensure any
thrown abort is handled so state is not set for cancelled/stale requests.

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.

[BUG]: Multiple users can register with the same display name causing matchmaking conflicts

1 participant