immich-app/immich · error · ForbiddenException

Cannot delete your own account

Error message

Cannot delete your own account

What it means

A ForbiddenException (HTTP 403) thrown by UserAdminService.delete when the authenticated admin's user id equals the target user id being deleted. Immich blocks self-deletion to prevent an admin from locking themselves out or orphaning the only admin account. The check runs after findOrFail but before any soft-delete side effects, so no state mutates.

Source

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

    if (dto.pinCode) {
      dto.pinCode = await this.cryptoRepository.hashBcrypt(dto.pinCode, SALT_ROUNDS);
    }

    if (dto.storageLabel === '') {
      dto.storageLabel = null;
    }

    const updatedUser = await this.userRepository.update(id, { ...dto, updatedAt: new Date() });

    return mapUserAdmin(updatedUser);
  }

  async delete(auth: AuthDto, id: string, dto: UserAdminDeleteDto): Promise<UserAdminResponseDto> {
    const { force } = dto;
    await this.findOrFail(id, {});
    if (auth.user.id === id) {
      throw new ForbiddenException('Cannot delete your own account');
    }

    await this.albumRepository.softDeleteAll(id);

    const status = force ? UserStatus.Removing : UserStatus.Deleted;
    const user = await this.userRepository.update(id, { status, deletedAt: new Date() });

    await this.eventRepository.emit('UserTrash', user);

    if (force) {
      await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } });
    }

    return mapUserAdmin(user);
  }

  async restore(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
    await this.findOrFail(id, { withDeleted: true });

View on GitHub (pinned to 199723261c)

Solutions

  1. Filter the calling user's id out of any bulk delete loop before issuing DELETE /admin/users/:id requests.
  2. In the admin UI, disable or hide the delete action for the row whose id equals the logged-in admin's id.
  3. If you genuinely need the account gone, have a different admin delete it, or transfer admin rights to another account first.
  4. Add a client-side guard: if (auth.user.id === targetId) skip the delete call and surface a message.

Example fix

// before
for (const id of userIds) {
  await adminApi.deleteUser(id); // throws 403 when id === me
}

// after
for (const id of userIds) {
  if (id === me.id) continue;
  await adminApi.deleteUser(id);
}
Defensive patterns

Strategy: validation

Validate before calling

function canDeleteUser(authUser, targetId) {
  if (authUser.id === targetId) {
    return { ok: false, reason: 'Cannot delete your own account' };
  }
  return { ok: true };
}
// before delete:
const check = canDeleteUser(auth, id);
if (!check.ok) { showError(check.reason); return; }

Type guard

const isSelfDelete = (authUserId: string, targetId: string) => authUserId === targetId;

Try / catch

try {
  await adminApi.deleteUser(id);
} catch (e) {
  if (e.status === 403 && e.message === 'Cannot delete your own account') {
    // skip self in bulk loop
    continue;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling DELETE /admin/users/:id where the :id path parameter matches the authenticated user's id (auth.user.id). Common with admin scripts that iterate over all user ids including the caller's own, or a UI that lists the current admin in the delete-eligible list.

Common situations: Bulk admin cleanup scripts that fetch all users and delete each one; frontend admin panels that do not filter the current user out of the delete list; automated test teardown that reuses the admin token for a self-delete call.

Related errors


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