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
- Have a different admin perform the isAdmin change
- Remove the isAdmin field from self-update payloads
- 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
- Strip isAdmin from self-update payloads
- Route admin-flag changes through a separate endpoint/flow requiring another admin
- In bulk updates, skip or sanitize the caller's own record
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)