gotify/server · error

cannot delete last admin

Error message

cannot delete last admin

What it means

Returned by DeleteUserByID with HTTP 400 when the deletion would remove the only remaining admin user. The handler counts users with Admin=true and, if the target is that last admin, refuses the delete to guarantee the system always retains at least one administrator able to manage users.

Source

Thrown at api/user.go:351

//	    schema:
//	        $ref: "#/definitions/Error"
//	  404:
//	    description: Not Found
//	    schema:
//	        $ref: "#/definitions/Error"
func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
	withID(ctx, "id", func(id uint) {
		user, err := a.DB.GetUserByID(id)
		if success := successOrAbort(ctx, 500, err); !success {
			return
		}
		if user != nil {
			adminCount, err := a.DB.CountUser(&model.User{Admin: true})
			if success := successOrAbort(ctx, 500, err); !success {
				return
			}
			if user.Admin && adminCount == 1 {
				ctx.AbortWithError(400, errors.New("cannot delete last admin"))
				return
			}
			if err := a.UserChangeNotifier.fireUserDeleted(id); err != nil {
				ctx.AbortWithError(500, err)
				return
			}
			successOrAbort(ctx, 500, a.DB.DeleteUserByID(id))
		} else {
			ctx.AbortWithError(404, errors.New("user does not exist"))
		}
	})
}

// ChangePassword changes the password from the current user
// swagger:operation POST /current/user/password user updateCurrentUser
//
// Update the password of the current user.
//

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Promote another user to admin first (PUT /api/users/{id} with admin:true), then delete the original
  2. Skip admin users in bulk-delete scripts (check user.Admin before DELETE)
  3. Create a dedicated break-glass admin account so routine admins can be deleted
  4. Re-order operations: demote the target to non-admin only after another admin exists

Example fix

// before
await api.deleteUserByID(adminId); // 400 cannot delete last admin
// after
await api.updateUserByID(otherId, { admin: true });
await api.deleteUserByID(adminId);
Defensive patterns

Strategy: validation

Validate before calling

const target = await api.getUserByID(id);
if (target && target.admin) {
  const admins = (await api.listUsers()).filter(u => u.admin);
  if (admins.length <= 1) {
    throw new Error('refusing to delete the last admin; promote another user first');
  }
}

Type guard

function isLastAdmin(user, allUsers) {
  return user != null && user.admin === true && allUsers.filter(u => u.admin).length === 1;
}

Try / catch

try {
  await api.deleteUserByID(id);
} catch (e) {
  if (e.status === 400 && /last admin/.test(e.message)) { await promoteReplacementAdmin(); await api.deleteUserByID(id); }
  else { throw e; }
}

Prevention

When it happens

Trigger: DELETE /api/users/{id} where the target user is admin and CountUser(&User{Admin:true}) == 1; cleanup scripts deleting all demo/staging users including the bootstrap admin; tenant offboarding that removes every account in one loop.

Common situations: Automated teardown scripts hitting the seeded admin account; single-admin organizations running deprovisioning flows; attempting to delete an admin before promoting a replacement; migrations that strip the admin flag from everyone then try deletion.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/f770c83a02b2ba7d. Report an issue: GitHub.