immich-app/immich · error · BadRequestException

Admin status can only be changed by another admin

Error message

Admin status can only be changed by another admin

What it means

Thrown (as BadRequestException) by UserAdminService.update when dto.isAdmin is defined, differs from the acting admin's current isAdmin flag, and the target id equals the acting admin's own id. This prevents an admin from demoting (or re-promoting) themselves and thereby locking the instance out of its last admin.

Source

Thrown at server/src/services/user-admin.service.ts:60

    await this.eventRepository.emit('UserSignup', {
      notify: !!notify,
      id: user.id,
      password: userDto.password,
    });

    return mapUserAdmin(user);
  }

  async get(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
    const user = await this.findOrFail(id, { withDeleted: true });
    return mapUserAdmin(user);
  }

  async update(auth: AuthDto, id: string, dto: UserAdminUpdateDto): Promise<UserAdminResponseDto> {
    const user = await this.findOrFail(id, {});

    if (dto.isAdmin !== undefined && dto.isAdmin !== auth.user.isAdmin && auth.user.id === id) {
      throw new BadRequestException('Admin status can only be changed by another admin');
    }

    if (dto.quotaSizeInBytes && user.quotaSizeInBytes !== dto.quotaSizeInBytes) {
      await this.userRepository.syncUsage(id);
    }

    if (dto.email) {
      const duplicate = await this.userRepository.getByEmail(dto.email);
      if (duplicate && duplicate.id !== id) {
        this.logger.debug('Email already in use by another account');
        throw new BadRequestException('Email is not available');
      }
    }

    if (dto.storageLabel) {
      const duplicate = await this.userRepository.getByStorageLabel(dto.storageLabel);
      if (duplicate && duplicate.id !== id) {
        throw new BadRequestException('Storage label already in use by another account');

View on GitHub (pinned to 199723261c)

Solutions

  1. Have a second admin perform the role change on this user.
  2. Omit isAdmin from the payload when editing your own account.
  3. If you are the only admin, promote another user first, then have them change your flag.

Example fix

// before (self demote)
await usersApi.update(myId, { isAdmin: false });
// after (another admin does it)
await usersApi.update(myId, { isAdmin: false }); // called by otherAdminAuth, not self
Defensive patterns

Strategy: validation

Validate before calling

function isSelfAdminToggle(authId: string, id: string, dto: UserAdminUpdateDto): boolean {
  return id === authId && dto.isAdmin !== undefined && dto.isAdmin !== auth.currentIsAdmin;
}
if (isSelfAdminToggle(auth.user.id, id, dto)) { /* block the action in the UI */ }

Try / catch

try { await usersApi.update(id, dto); }
catch (e) {
  if (e instanceof BadRequestException && /Admin status can only be changed/.test(e.message)) {
    // route through another admin or drop isAdmin from the payload
  }
}

Prevention

When it happens

Trigger: PUT /admin/users/:id where :id is the caller's own id and dto.isAdmin is set to a value different from their current admin flag.

Common situations: Admin tries to demote themselves via the UI; automation bulk-toggles isAdmin and includes the actor; self-toggle through a generic update form.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/4d2e3699e863c1ce. Report an issue: GitHub.