{"record":{"id":"f7487e4d781b5615","repo":"hcengineering/platform","slug":"account-status-forbidden","errorCode":"account.status.Forbidden","errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"error_code","errorClass":"PlatformError","httpStatus":null,"severity":"error","filePath":"server/account/src/operations.ts","lineNumber":3014,"sourceCode":"    }\n  }\n\n  if (personUuid == null) {\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))\n  }\n\n  if (!allowedService) {\n    // User should always have at least one Huly and one \"login\" social id\n    // so do not allow releasing last ones\n    const socialIds = await db.socialId.find({ personUuid, verifiedOn: { $gt: 0 }, isDeleted: { $ne: true } })\n    const afterRemoval = socialIds.filter((it) => it.type !== type || it.value !== value)\n\n    if (afterRemoval.filter((it) => it.type === SocialIdType.HULY).length === 0) {\n      throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))\n    }\n\n    if (afterRemoval.filter((it) => loginSocialTypes.includes(it.type)).length === 0) {\n      throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))\n    }\n  }\n\n  return await doReleaseSocialId(db, personUuid, type, value, extra?.service ?? account, deleteIntegrations)\n}\n\nexport async function deleteAccount (\n  ctx: MeasureContext,\n  db: AccountDB,\n  branding: Branding | null,\n  token: string,\n  params: { uuid?: AccountUuid }\n): Promise<void> {\n  const { extra } = decodeTokenVerbose(ctx, token)\n\n  const isAdmin = extra?.admin === 'true'\n\n  if (!isAdmin) {","sourceCodeStart":2996,"sourceCodeEnd":3032,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/server/account/src/operations.ts#L2996-L3032","documentation":"Thrown when removing a social id from an account would leave it in an invalid state. After computing the social ids that would remain ('afterRemoval'), the account service refuses the release if no HULY social id remains or if no login-capable social id remains. This guarantees every account keeps a working login method.","triggerScenarios":"Calling the release/remove social id operation (doReleaseSocialId path in server/account/src/operations.ts) when it would remove the last HULY-type social id, or the last social id whose type is in the allowed loginSocialTypes list.","commonSituations":"A user tries to unlink their only Huly login or last email/GitHub identity; scripts cleaning up social integrations delete all ids at once; admin tooling removes a social id without first adding a replacement.","solutions":["Add a new login-capable social id to the account before removing the current one","Remove a different (non-last) social id instead","If the removal is intentional, first release other ids then re-add a valid login id immediately after in the same administrative flow"],"exampleFix":"// before\nawait client.removeSocialId(token, SocialIdType.GITHUB, value) // account has only this login id\n// after\nawait client.addSocialId(token, SocialIdType.EMAIL, newEmail)\nawait client.removeSocialId(token, SocialIdType.GITHUB, value)","handlingStrategy":"validation","validationCode":"const remaining = socialIds.filter(id => id.value !== removingValue)\nconst loginTypes = [SocialIdType.HULY, SocialIdType.EMAIL, SocialIdType.GITHUB]\nif (remaining.filter(i => i.type === SocialIdType.HULY).length === 0 ||\n    remaining.filter(i => loginTypes.includes(i.type)).length === 0) {\n  throw new Error('cannot remove: account must keep a HULY id and at least one login id')\n}","typeGuard":"function hasSafeRemaining(ids: SocialId[], removing: SocialId): boolean {\n  const rest = ids.filter(i => i._id !== removing._id)\n  return rest.some(i => i.type === SocialIdType.HULY) && rest.some(i => LOGIN_TYPES.includes(i.type))\n}","tryCatchPattern":"try {\n  await accountClient.releaseSocialId(token, type, value)\n} catch (err) {\n  if ((err as PlatformError).props?.code === platform.status.Forbidden) {\n    console.error('cannot remove last login social id; add a replacement first')\n  } else throw err\n}","preventionTips":["Always add a replacement social id before removing the current login id","Query the account's social ids first and compute the post-removal set","Block removal of HULY-type ids in UI for accounts with only one"],"tags":["forbidden","account","social-id","authentication"],"backgroundTag":"no-login-social-id-remaining","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}