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 by UserAdminService.update when a user attempts to change their own isAdmin flag. Only another admin may grant or revoke admin status, preventing an admin from irreversibly altering their own privileges (e.g. demoting themselves). It is a 400 BadRequest for a self-modification attempt.

Solutions

  1. Have a different admin perform the isAdmin change
  2. Remove the isAdmin field from self-update payloads
  3. Split the profile form so privilege fields are only sent when editing other users

Example fix

// before
await adminApi.updateUser(myId, { name, isAdmin: false });
// after
await adminApi.updateUser(myId, { name }); // omit isAdmin for self-updates
Defensive patterns

Strategy: validation

Validate before calling

if (dto.isAdmin !== undefined && id === auth.user.id && dto.isAdmin !== auth.user.isAdmin) {
  throw new Error('cannot change your own admin status');
}

Try / catch

try { await adminApi.updateUser(id, dto); } catch (e) {
  if (e.response?.status === 400 && /Admin status/.test(e.response?.data?.message ?? '')) {
    const { isAdmin, ...rest } = dto;
    return adminApi.updateUser(id, rest); // retry without isAdmin
  }
  throw e;
}

Prevention

When it happens

Trigger: PUT/PATCH /api/admin/users/:id where id === auth.user.id and dto.isAdmin is defined and differs from the caller's current isAdmin value.

Common situations: Admin editing their own profile in the UI and accidentally toggling the admin switch; bulk-update scripts that include isAdmin on every user including the caller; self-service profile forms submitting the full user object.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/4d2e3699e863c1ce. Report an issue: GitHub.

Appendix: 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 e55ac299a4)