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
- Have a second admin perform the role change on this user.
- Omit isAdmin from the payload when editing your own account.
- 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
- Disable the 'isAdmin' toggle in the UI for the current user's own row.
- Strip isAdmin from self-update payloads client-side.
- Maintain at least two admins so role changes never deadlock.
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
- You may not access another user's locked timeline
- password is required
- Email is not available
- Storage label already in use by another account
- Forbidden
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/4d2e3699e863c1ce.
Report an issue: GitHub.