Skip to content

fix(profile): safely handle already-linked identity conflicts#1056

Merged
fayazg merged 6 commits into
mainfrom
fix/LFXV2-2569
Jul 2, 2026
Merged

fix(profile): safely handle already-linked identity conflicts#1056
fayazg merged 6 commits into
mainfrom
fix/LFXV2-2569

Conversation

@fayazg

@fayazg fayazg commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Return a generic, non-identifying message when an email is already linked to another account (send-code + verify), and log the resolved account server-side for support.
  • Surface the failure in the add-account dialog's email-entry step (previously silent).
  • Fix social (GitHub/Google) already-linked detection so the conflict banner shows instead of a misleading generic "try again" toast.
  • Centralize conflict detection in the shared isIdentityAlreadyLinkedError util (with unit tests) and reuse a shared EMAIL_ALREADY_LINKED_MESSAGE constant across paths.

Fixes LFXV2-2569

🤖 Generated with Claude Code

fayazg added 3 commits July 1, 2026 17:38
Return a generic message from the send-code and verify endpoints when an
email is already linked to another account, and log the resolved account
server-side. Surface the error in the add-account dialog's email step so
the failure is visible to the user.

Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com>
Jira: LFXV2-2569
Extract the generic already-linked response string into
EMAIL_ALREADY_LINKED_MESSAGE in the shared profile constants and reuse it
in both the send-code and verify branches, so the two responses stay in
sync.

Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com>
Jira: LFXV2-2569
The identity-link call returns "the provided identity token belongs to an
existing LFID account and cannot be linked", which has no "already", so the
social callback misclassified it as a generic link_failed toast instead of
the already-linked conflict banner. Centralize conflict detection in
isIdentityAlreadyLinkedError and apply it to the send-code, verify, and
social-callback paths.

Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com>
Jira: LFXV2-2569
Copilot AI review requested due to automatic review settings July 1, 2026 23:39
@fayazg fayazg requested a review from a team as a code owner July 1, 2026 23:39
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 966e67ca-90de-412c-8cbb-97b7488aa5a3

📥 Commits

Reviewing files that changed from the base of the PR and between 8fd1b88 and f5ddf23.

📒 Files selected for processing (3)
  • apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html
  • apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.ts
  • apps/lfx-one/src/server/controllers/profile.controller.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html
  • apps/lfx-one/src/server/controllers/profile.controller.ts

Walkthrough

Adds a shared helper and message constant for already-linked identity errors, updates profile controller flows to use them, and shows an inline add-account dialog error when verification fails before a code is sent.

Changes

Already-linked identity error handling

Layer / File(s) Summary
Shared detection helper and message
packages/shared/src/utils/identity.utils.ts, packages/shared/src/utils/identity.utils.spec.ts, packages/shared/src/constants/profile.constants.ts
Adds isIdentityAlreadyLinkedError, a generic EMAIL_ALREADY_LINKED_MESSAGE, and tests for email-shape and already-linked error matching.
Controller already-linked handling
apps/lfx-one/src/server/controllers/profile.controller.ts
Uses the shared helper and message in the social callback and email link flows, and returns HTTP 409 with the standardized message for already-linked cases.
Add-account dialog error display
apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html, apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.ts
Adds an inline send-code error message, marks the OTP error as an alert, and prefers response messages when storing verification errors.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed It accurately summarizes the main change: safer handling of already-linked identity conflicts in profile flows.
Description check ✅ Passed It matches the implemented email, social, shared util, and UI fixes described in the diff.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/LFXV2-2569

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 hardens and unifies “identity already linked” conflict handling across the shared utilities, the profile server controller, and the add-account dialog UI, with an emphasis on preventing account enumeration while still enabling server-side support diagnostics.

Changes:

  • Centralized upstream conflict detection in isIdentityAlreadyLinkedError (shared util) and added unit tests.
  • Introduced EMAIL_ALREADY_LINKED_MESSAGE and updated profile controller endpoints to return a generic message while logging the resolved owning account server-side.
  • Surfaced send-code failures in the add-account dialog’s email-entry step so conflicts aren’t silently ignored.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/shared/src/utils/identity.utils.ts Adds shared marker-based helper to detect already-linked identity/email conflicts across differing upstream phrasings.
packages/shared/src/utils/identity.utils.spec.ts Adds Vitest coverage for the new conflict detector and existing email-shape helper.
packages/shared/src/constants/profile.constants.ts Adds a reusable generic “email already linked” client message to avoid account enumeration.
apps/lfx-one/src/server/controllers/profile.controller.ts Uses shared conflict detection, returns generic conflict messaging (409) for send-code + verify, and logs resolved account server-side for support.
apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html Displays verification/send errors during the email-entry step (previously silent).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/lfx-one/src/server/controllers/profile.controller.ts (1)

1925-1927: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing support-log parity with sendEmailVerification.

sendEmailVerification's already-linked branch resolves and logs the linked account (linked_to) for support debugging. verifyAndLinkEmail's equivalent branch returns the generic message directly without the same resolution/logging, losing support visibility when the conflict surfaces at the verify step instead of send-code.

♻️ Proposed fix for parity
       if (!linkResponse.success) {
         if (isIdentityAlreadyLinkedError(linkResponse.error, linkResponse.message)) {
+          const linkedTo =
+            (await this.emailVerificationService.resolveEmailToUsername(req, email)) || (await this.emailVerificationService.resolveEmailToSub(req, email));
+          logger.warning(req, 'verify_and_link_email', 'Email already linked to another account', { email, linked_to: linkedTo });
           // Never forward the upstream message here — it could name the owning account.
           res.status(409).json({ success: false, error: linkResponse.error, message: EMAIL_ALREADY_LINKED_MESSAGE });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/controllers/profile.controller.ts` around lines 1925
- 1927, The already-linked branch in verifyAndLinkEmail lacks the same
support-log parity as sendEmailVerification, so add the linked-account
resolution and logging before returning the 409 response. Use the existing
identity-link flow helpers around isIdentityAlreadyLinkedError and the
linkResponse handling to resolve the owning account into linked_to, log it for
support debugging, and still return the generic EMAIL_ALREADY_LINKED_MESSAGE
without exposing upstream details.
apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html (1)

95-98: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add an ARIA live region for the new error message.

The error <p> has no role="alert"/aria-live, so screen reader users won't be notified when the already-linked error appears.

♿ Proposed fix
 `@if` (!codeSent() && verificationError()) {
-  <p class="text-sm text-red-600" data-testid="add-account-send-error">{{ verificationError() }}</p>
+  <p class="text-sm text-red-600" role="alert" aria-live="polite" data-testid="add-account-send-error">{{ verificationError() }}</p>
 }

As per path instructions, apps/lfx-one/src/app/**/*.component.{ts,html} changes implementing error states should "add ARIA roles, focus management, ... interactive states (hover, active, loading, error, empty)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html`
around lines 95 - 98, The new verification error message in
add-account-dialog.component.html needs an ARIA live region so screen readers
announce it when it appears. Update the error <p> rendered by the
verificationError() branch to include an appropriate alert/live attribute (for
example, role="alert" or aria-live) while keeping the existing data-testid and
conditional rendering intact. Use the add-account-dialog.component.html template
and the verificationError() / codeSent() block to locate the change.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/lfx-one/src/server/controllers/profile.controller.ts`:
- Around line 1796-1809: The 409 response in profile.controller.ts still exposes
the raw upstream error via response.error, which can override the intended
EMAIL_ALREADY_LINKED_MESSAGE in add-account-dialog.component.ts. Update the
send_email_verification and related linked-account response paths to return a
stable error code or a sanitized error value instead of
response.error/linkResponse.error, and ensure the client-side handling in
add-account-dialog.component.ts prefers the response message for this case.

---

Nitpick comments:
In
`@apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html`:
- Around line 95-98: The new verification error message in
add-account-dialog.component.html needs an ARIA live region so screen readers
announce it when it appears. Update the error <p> rendered by the
verificationError() branch to include an appropriate alert/live attribute (for
example, role="alert" or aria-live) while keeping the existing data-testid and
conditional rendering intact. Use the add-account-dialog.component.html template
and the verificationError() / codeSent() block to locate the change.

In `@apps/lfx-one/src/server/controllers/profile.controller.ts`:
- Around line 1925-1927: The already-linked branch in verifyAndLinkEmail lacks
the same support-log parity as sendEmailVerification, so add the linked-account
resolution and logging before returning the 409 response. Use the existing
identity-link flow helpers around isIdentityAlreadyLinkedError and the
linkResponse handling to resolve the owning account into linked_to, log it for
support debugging, and still return the generic EMAIL_ALREADY_LINKED_MESSAGE
without exposing upstream details.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9fc7257-b15c-4b20-84bf-6248b78ceddd

📥 Commits

Reviewing files that changed from the base of the PR and between 663837c and 8fd1b88.

📒 Files selected for processing (5)
  • apps/lfx-one/src/app/modules/profile/components/add-account-dialog/add-account-dialog.component.html
  • apps/lfx-one/src/server/controllers/profile.controller.ts
  • packages/shared/src/constants/profile.constants.ts
  • packages/shared/src/utils/identity.utils.spec.ts
  • packages/shared/src/utils/identity.utils.ts

Comment thread apps/lfx-one/src/server/controllers/profile.controller.ts
Address review comments from coderabbitai:

- add-account-dialog.component.ts: prefer response.message over response.error
  in the send/verify/resend success:false callbacks so the generic message wins
- profile.controller.ts: log the resolved owning account (linked_to) in
  verifyAndLinkEmail's already-linked branch, matching sendEmailVerification
- add-account-dialog.component.html: add role="alert" to the send/OTP error
  messages so screen readers announce them

Resolves 3 review threads.

Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com>
Jira: LFXV2-2569
@fayazg

fayazg commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: f5ddf23

Changes Made

  • add-account-dialog.component.ts: the send/verify/resend success:false callbacks now prefer response.message over response.error, so the generic already-linked message always wins over the raw upstream code (per coderabbitai). The retained error value carries no LFID, and the real already-linked path (HTTP 409) already routed through the error callback that preferred message — this makes the ordering consistent across all paths.
  • profile.controller.ts: verifyAndLinkEmail's already-linked branch now resolves the owning account and logs it as linked_to, matching sendEmailVerification's support-logging parity (per coderabbitai).
  • add-account-dialog.component.html: added role="alert" to the send-error and OTP-error messages so screen readers announce them, matching the repo's impersonation-error pattern (per coderabbitai).

Threads Resolved

1 of 1 review threads addressed; the two nitpicks from the review body are addressed in the same commit.

@dealako

dealako commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hey @fayazg 👋 — reviewed this round (commit f5ddf233 onward) covering the identity-already-linked account-enumeration fix.

Overall impression: This is a clean, well-scoped fix. The core mitigation — never surfacing the owning account's identity to the client while still logging it server-side for support — is applied consistently across all three conflict paths (send-code, verify-and-link, and the social auth callback). The centralized isIdentityAlreadyLinkedError matcher and EMAIL_ALREADY_LINKED_MESSAGE constant are good calls; they replace what was previously an inconsistent, path-specific substring check. New tests in identity.utils.spec.ts cover both upstream phrasings, case-insensitivity, and negative cases.

I ran two independent subagent passes (security-focused and general code-quality) in addition to my own review across all changed files.

Revision tracking — round 1 → round 2:
All three of CodeRabbit's round-1 findings are resolved in f5ddf233, verified against current code:

  • response.error/response.message fallback ordering flipped in all three add-account-dialog.component.ts callbacks, so the generic message now wins over any raw upstream error text.
  • ✅ Support-log parity — verifyAndLinkEmail now logs linked_to the same way sendEmailVerification does.
  • role="alert" added to both new error <p> elements in the dialog HTML.

No regressions introduced by the fix-up commit. I agree with Copilot's clean pass on this PR.

One thing I'd note but don't think needs action: CodeRabbit's original ARIA suggestion also proposed aria-live="polite", but only role="alert" landed. That's fine — role="alert" already implies an assertive live region, so adding aria-live="polite" on top would be redundant, not a gap.

Security pass: No enumeration vector in any of the three paths (send-code, verify-and-link, social callback) — all return the generic message/redirect code, never the owning account. Auth/CSRF checks are intact. One secondary observation: the resolveEmailToUsername/resolveEmailToSub calls only run on the already-linked branch, creating a small latency difference vs. other failure paths. This is a low-severity, pre-existing-shape timing signal (the response body/status code carry no identifying info either way) — not a blocker, just worth knowing about if enumeration-hardening ever gets revisited.

Code quality: One optional nit — the three-line linkedTo resolution pattern is now duplicated identically in sendEmailVerification and verifyAndLinkEmail (profile.controller.ts:1801-1802 and 1927-1928). Could be extracted to a small private helper, but it's simple enough that leaving it inline is also fine — take-it-or-leave-it.

Issue count:

  • 🔴 Blocking: 0
  • 🟡 Minor: 0
  • ⚪ Nit: 1 — optional extraction of the duplicated linkedTo resolution snippet into a helper

Final decision:Approved

Nice work tightening this up — the enumeration fix is solid and the round-1 feedback was addressed thoroughly.

@dealako dealako left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved — no blocking issues, all round-1 CodeRabbit findings verified resolved in f5ddf23, no merge conflicts. See consolidated summary comment for full details.

Copilot AI review requested due to automatic review settings July 2, 2026 17:17
@fayazg fayazg merged commit c23fa04 into main Jul 2, 2026
8 checks passed
@fayazg fayazg deleted the fix/LFXV2-2569 branch July 2, 2026 17:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

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.

3 participants