{"record":{"id":"29ee4f72bb849f1f","repo":"Stirling-Tools/Stirling-PDF","slug":"member-must-belong-to-a-team-to-become-its-owner","errorCode":null,"errorMessage":"Member must belong to a team to become its owner","messagePattern":"Member must belong to a team to become its owner","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/api/users.ts","lineNumber":370,"sourceCode":"    // Demote to web-only; drop any leadership first so no team is left ownerless-by-a-guest.\n    if (holdsLeader && member.teamId) {\n      await apiClient.local.form(\"/api/v1/team/removeOwner\", {\n        teamId: String(member.teamId),\n        userId: member.id,\n      });\n    }\n    await setAuthority(member.username, \"ROLE_WEB_ONLY_USER\");\n    return;\n  }\n\n  if (target === \"admin\") {\n    if (!holdsAdmin) await setAuthority(member.username, \"ROLE_ADMIN\");\n    return; // any LEADER membership is harmless; an admin owns everything anyway\n  }\n\n  if (target === \"team_owner\") {\n    if (!member.teamId) {\n      throw new Error(\"Member must belong to a team to become its owner\");\n    }\n    // Assign ownership first so a 400 (e.g. system team) leaves the user unchanged.\n    await apiClient.local.form(\"/api/v1/team/setOwner\", {\n      teamId: String(member.teamId),\n      userId: member.id,\n    });\n    if (holdsAdmin || holdsWebOnly)\n      await setAuthority(member.username, \"ROLE_USER\");\n    return;\n  }\n\n  // target === \"member\": drop any leadership, and normalise a non-ROLE_USER authority.\n  if (holdsLeader && member.teamId) {\n    await apiClient.local.form(\"/api/v1/team/removeOwner\", {\n      teamId: String(member.teamId),\n      userId: member.id,\n    });\n  }","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/api/users.ts#L352-L388","documentation":"Thrown by changeMemberRole() (users.ts) when the target role is 'team_owner' but member.teamId is falsy. Team ownership is assigned via POST /api/v1/team/setOwner which requires a teamId, so promoting a teamless member to owner is impossible. The guard runs before any backend call, leaving the user unchanged.","triggerScenarios":"An admin attempts to set a member's role to 'team_owner' for a member whose teamId is undefined/null (not assigned to any team).","commonSituations":"A member was created/invited without a team; the member's team was dissolved leaving teamId stale/null; the UI allowed selecting 'team_owner' for a teamless member.","solutions":["Assign the member to a team first (moveMemberToTeam), then retry the role change to team_owner.","Disable the 'team_owner' role option in the UI for members without a teamId.","Validate member.teamId before calling changeMemberRole and surface a clear inline message."],"exampleFix":"// before — throws for teamless members\nif (target === \"team_owner\") {\n  if (!member.teamId) throw new Error(\"Member must belong to a team to become its owner\");\n  ...\n}\n\n// after — guard at the call site, block the action in the UI\nasync function promoteToOwner(member: Member) {\n  if (!member.teamId) throw new TeamRequiredError(member.id);\n  return changeMemberRole(member, \"team_owner\");\n}","handlingStrategy":"validation","validationCode":"import type { Member } from \"@portal/api/users\";\nfunction canBeTeamOwner(member: Member): boolean { return member.teamId != null; }\n// before changeMemberRole: if (target === 'team_owner' && !canBeTeamOwner(member)) blockUI();","typeGuard":"function isTeamRequired(e: unknown): boolean {\n  return e instanceof Error && /must belong to a team/.test(e.message);\n}","tryCatchPattern":"try { await changeMemberRole(member, 'team_owner'); }\ncatch (e) { if (isTeamRequired(e)) { notify('Assign the member to a team first.'); return; } throw e; }","preventionTips":["Disable the 'team_owner' role option in the UI for members without a teamId.","Assign the member to a team (moveMemberToTeam) before promoting.","Validate member.teamId at the call site to give a clear inline message."],"tags":["portal","users","team-management","validation","roles"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}