Stirling-Tools/Stirling-PDF · error · Error

Member has no backend identity

Error message

Member has no backend identity

What it means

Thrown by setMemberSuspended() when member.username is falsy. The suspension call hits /api/v1/user/admin/changeUserEnabled/{username}, which requires a real backend username. Members without a username are pure fixtures/invited-not-yet-provisioned rows that have no backend identity, so suspending them is impossible.

Source

Thrown at frontend/editor/src/portal/api/users.ts:397

    return;
  }

  // target === "member": drop any leadership, and normalise a non-ROLE_USER authority.
  if (holdsLeader && member.teamId) {
    await apiClient.local.form("/api/v1/team/removeOwner", {
      teamId: String(member.teamId),
      userId: member.id,
    });
  }
  if (holdsAdmin || holdsWebOnly)
    await setAuthority(member.username, "ROLE_USER");
}

export async function setMemberSuspended(
  member: Member,
  suspended: boolean,
): Promise<void> {
  if (!member.username) throw new Error("Member has no backend identity");
  await apiClient.local.form(
    `/api/v1/user/admin/changeUserEnabled/${encodeURIComponent(member.username)}`,
    { enabled: String(!suspended) },
  );
}

export async function removeMember(member: Member): Promise<void> {
  if (!member.username) throw new Error("Member has no backend identity");
  await apiClient.local.json(
    `/api/v1/user/admin/deleteUser/${encodeURIComponent(member.username)}`,
    { method: "POST" },
  );
}

export interface ResetPasswordOptions {
  /** The new password (client-generated or admin-typed). */
  newPassword: string;
  forcePasswordChange?: boolean;

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Check member.username (and member.status) before offering the suspend action; hide it for invited/fixture members.
  2. If the member should have a username, re-run provisioning/invitation acceptance first.
  3. Validate at the call site and surface 'this member has no backend account yet' to the admin.

Example fix

// before — throws for members with no backend identity
export async function setMemberSuspended(member: Member, suspended: boolean) {
  if (!member.username) throw new Error("Member has no backend identity");
  ...
}

// after — typed error + UI gating on username presence
if (!member.username) throw new NoBackendIdentityError(member.id);
// UI: disable the Suspend button when !member.username
Defensive patterns

Strategy: validation

Validate before calling

import type { Member } from "@portal/api/users";
function hasBackendIdentity(member: Member): boolean { return !!member.username; }
// before suspend: if (!hasBackendIdentity(member)) blockSuspend();

Type guard

function isNoBackendIdentity(e: unknown): boolean {
  return e instanceof Error && /no backend identity/i.test(e.message);
}

Try / catch

try { await setMemberSuspended(member, true); }
catch (e) { if (isNoBackendIdentity(e)) { notify('Member has no backend account yet.'); return; } throw e; }

Prevention

When it happens

Trigger: An admin attempts to suspend a member whose username is undefined — an invited-but-not-provisioned member, a demo fixture, or a row that never completed backend provisioning.

Common situations: Member is in 'invited' status and never accepted (no backend user created); a fixture-only member loaded in a demo/mock context; the roster sync dropped the username field.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/86467ab5adc307f8. Report an issue: GitHub.