{"record":{"id":"d82d855448690b96","repo":"yikart/AiToEarn","slug":"channelaccountalreadyconnectedtoanotheruser","errorCode":"ChannelAccountAlreadyConnectedToAnotherUser","errorMessage":"ResponseCode.ChannelAccountAlreadyConnectedToAnotherUser","messagePattern":"ResponseCode\\.ChannelAccountAlreadyConnectedToAnotherUser","errorType":"exception","errorClass":"AppException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/auth/auth.service.ts","lineNumber":752,"sourceCode":"    let account = await this.accountRepo.getByIdentity(identity)\n    let created = false\n    if (!account) {\n      account = await this.accountRepo.createByIdentity(identity, accountData)\n      created = true\n    }\n    if (!created && account && (account.userId === input.userId || !account.userId || input.allowReassign)) {\n      account = await this.accountRepo.updateByIdentity(identity, accountData) ?? account\n    }\n\n    if (!account) {\n      throw new AppException(ResponseCode.AccountCreateFailed)\n    }\n    if (account.userId !== input.userId) {\n      this.logger.warn(\n        { input, existingAccount: account },\n        'Channel account already connected to another user',\n      )\n      throw new AppException(ResponseCode.ChannelAccountAlreadyConnectedToAnotherUser)\n    }\n\n    return account\n  }\n\n  private async saveSelectableCredential(\n    accountId: string,\n    platform: AccountType,\n    credential: PlatformAccountCredentialSnapshot,\n  ): Promise<void> {\n    await this.credentialService.saveCredential(accountId, platform, {\n      accessToken: credential.accessToken,\n      refreshToken: credential.refreshToken,\n      expiresAt: credential.expiresAt,\n      scope: credential.scope,\n    })\n  }\n","sourceCodeStart":734,"sourceCodeEnd":770,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/auth/auth.service.ts#L734-L770","documentation":"createOrUpdateAccount throws ChannelAccountAlreadyConnectedToAnotherUser (AppException, ResponseCode.ChannelAccountAlreadyConnectedToAnotherUser) when the platform identity already exists but belongs to a different userId, and the update path was skipped (no allowReassign) so account.userId !== input.userId. This is an ownership conflict guard preventing one user from hijacking a channel account connected by another user. A warning is logged with the input and existing account before throwing.","triggerScenarios":"Two AiToEarn users authorizing the same platform account (same platform+uid); re-running OAuth for a channel previously connected by a teammate without allowReassign=true; YouTube accounts distinguished by handle where the same channel maps to an existing row owned elsewhere.","commonSituations":"Agency accounts (company TikTok/YouTube) connected by employee A, then employee B tries to connect the same one; staging data copied from prod retaining another user's ownership; testing with a colleague's platform account.","solutions":["If reassignment is intended, set allowReassign=true in the auth input so the existing account is transferred to the new user.","Have the current owner disconnect the account first, then reconnect under the new user.","Use a different platform account for the new user, or accept the error and surface 'account already connected to another user' in the UI.","Verify which userId owns the identity (query by identity) before initiating the OAuth flow to avoid surprise conflicts."],"exampleFix":"// before\nawait authService.handleAuthCallback({ userId, platform, ... }) // throws if owned by other user\n// after\nawait authService.handleAuthCallback({ userId, platform, allowReassign: true, ... })","handlingStrategy":"try-catch","validationCode":"const existing = await accountRepo.getByIdentity({ type: platform, uid: platformUid, account })\nif (existing && existing.userId && existing.userId !== userId && !allowReassign) {\n  throw new AppException(ResponseCode.ChannelAccountAlreadyConnectedToAnotherUser)\n}","typeGuard":"function ownedByUser(a: { userId?: string } | null, userId: string): boolean {\n  return !!a && (!a.userId || a.userId === userId)\n}","tryCatchPattern":"try {\n  await authService.handleAuthCallback(input)\n} catch (e) {\n  if (e instanceof AppException && e.code === ResponseCode.ChannelAccountAlreadyConnectedToAnotherUser) {\n    return res.status(409).json({ message: 'This channel account is already connected to another user' })\n  }\n  throw e\n}","preventionTips":["Check ownership of the platform identity before starting the OAuth flow.","Require explicit allowReassign=true (and user confirmation) for account takeover.","In teams, document which member owns shared channel accounts.","Return 409-style conflicts so clients can distinguish ownership issues from technical failures."],"tags":["ownership-conflict","authorization","channels"],"backgroundTag":"account-already-connected","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}