invoke-ai/InvokeAI · error · HTTPException

Cannot remove the last administrator

Error message

Cannot remove the last administrator

What it means

HTTP 400 raised by update_user when the change would demote or deactivate the last active administrator (auth.py:603). Authorization is derived from the database on every request, so removing the final admin would irreversibly lock everyone out of administration; LAST_ADMIN_DETAIL communicates this.

Source

Thrown at invokeai/app/api/routers/auth.py:603

        request.is_active is False or request.is_admin is True or request.password is not None
    ):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=SYSTEM_USER_PROTECTED_DETAIL,
        )

    # Demoting or deactivating the last administrator is irreversible: authorization is
    # derived from the database on every request, so the caller loses admin access
    # immediately and no authenticated path back exists. It would also drop `has_admin()`
    # to zero, which re-opens the unauthenticated `/auth/setup` endpoint to any caller.
    # `delete_user` guards the same invariant.
    if (
        before.is_admin
        and before.is_active
        and (request.is_admin is False or request.is_active is False)
        and user_service.count_admins() <= 1
    ):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=LAST_ADMIN_DETAIL,
        )

    try:
        changes = UserUpdateRequest(
            display_name=request.display_name,
            password=request.password,
            is_admin=request.is_admin,
            is_active=request.is_active,
        )
        updated = user_service.update(user_id, changes, strict_password_checking=config.strict_password_checking)
    except ValueError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e

    # Authorization state changed — notify live connections (open sockets, the
    # session processor) so demotion/deactivation takes effect immediately
    # instead of persisting until reconnect or token expiry. A password reset bumps

View on GitHub (pinned to 0b6a024f2f)

Solutions

  1. Promote another user to admin first (PATCH {"is_admin": true}), then demote/deactivate the original admin.
  2. Reorder bulk operations: grant new admins before removing old ones.
  3. Skip the last active admin in cleanup scripts.
  4. If already locked out at the DB level, restore from backup or fix is_admin flags directly in the users table as a last resort.

Example fix

// before: demote before promoting a successor
await api.patch(`/users/${oldAdmin}`, {is_admin:false});
await api.patch(`/users/${newAdmin}`, {is_admin:true}); // 400: last admin
// after: promote successor first
await api.patch(`/users/${newAdmin}`, {is_admin:true});
await api.patch(`/users/${oldAdmin}`, {is_admin:false});
Defensive patterns

Strategy: validation

Validate before calling

async function canRemoveAdmin(id) {
  const users = await (await fetch('/api/v1/users')).json();
  const target = users.find(u => u.id === id);
  const activeAdmins = users.filter(u => u.is_admin && u.is_active);
  if (target?.is_admin && target.is_active && activeAdmins.length <= 1) {
    return ['cannot demote/deactivate the last active administrator'];
  }
  return []; // [] => safe
}

Try / catch

try {
  await api.patch(`/users/${id}`, {is_admin:false});
} catch (e) {
  if (e.status === 400 && e.detail.includes('last administrator')) {
    throw new Error('Promote another admin before demoting this one');
  }
  throw e;
}

Prevention

When it happens

Trigger: PATCH /api/v1/users/{id} on the sole active admin with either "is_admin": false or "is_active": false, while `user_service.count_admins() <= 1`.

Common situations: Single-admin installs where the owner tries to demote themselves; cleanup scripts deactivating all users including the only admin; transferring admin rights by demoting before the new admin was promoted.

Related errors


AI-assisted analysis of invoke-ai/InvokeAI@0b6a024f2f (2026-08-29). Data as JSON: /api/errors/267fa90ceb916aa3. Report an issue: GitHub.