immich-app/immich · error · BadRequestException
User already has a PIN code
Error message
User already has a PIN code
What it means
setupPinCode only allows a user to set a PIN once. If the loaded user already has a pinCode stored, it refuses to overwrite it with a 400 'User already has a PIN code'; changing an existing PIN must go through changePinCode, and removal through resetPinCode.
Solutions
- Use the change PIN endpoint (changePinCode) instead of setup to update an existing PIN.
- If the PIN is unknown, use the reset PIN flow (resetPinCode) which authenticates with the account password.
- Check GET /api/auth/pin-code/status before calling setup to see whether a PIN already exists.
Example fix
// before
await api.authenticationApi.setupPinCode({ pinCode }); // PIN already set
// after
const { hasPin } = await api.authenticationApi.getPinCodeStatus();
if (hasPin) {
await api.authenticationApi.changePinCode({ pinCode: currentPin, newPinCode });
} else {
await api.authenticationApi.setupPinCode({ pinCode });
} Defensive patterns
Strategy: validation
Validate before calling
const status = await api.authenticationApi.getPinCodeStatus();
if (status.hasPin) {
throw new Error('PIN already set; use changePinCode or resetPinCode instead of setup');
} Try / catch
try {
await api.authenticationApi.setupPinCode({ pinCode });
} catch (e) {
if (e.status === 400 && e.message === 'User already has a PIN code') {
// fall back to change/reset flow
}
throw e;
} Prevention
- Always check PIN status before choosing setup vs change vs reset.
- Disable the setup UI once a PIN exists; idempotent-guard against double submit.
- Keep PIN state in sync across devices after setup.
When it happens
Trigger: Calling POST /api/auth/pin-code/setup for a user whose record already has a non-null pinCode hash.
Common situations: Double-submitting the setup request (e.g. retry after timeout when the first call actually succeeded); calling setup instead of change when rotating the PIN; client state out of sync after the PIN was set on another device.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- Either password or pinCode is required
- User does not have a PIN code
- Asset dimensions are not available for editing
- assetIds, albumId, or userId is required
- At least two people are required for merging
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15).
Data as JSON: /api/errors/cb51d4ae03320a73.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/auth.service.ts:159
});
await this.eventRepository.emit('AuthChangePassword', {
userId: user.id,
currentSessionId: auth.session?.id,
invalidateSessions: dto.invalidateSessions,
});
return mapUserAdmin(updatedUser);
}
async setupPinCode(auth: AuthDto, { pinCode }: PinCodeSetupDto) {
const user = await this.userRepository.getForPinCode(auth.user.id);
if (!user) {
throw new UnauthorizedException();
}
if (user.pinCode) {
throw new BadRequestException('User already has a PIN code');
}
const hashed = await this.cryptoRepository.hashBcrypt(pinCode, SALT_ROUNDS);
await this.userRepository.update(auth.user.id, { pinCode: hashed });
}
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
const user = await this.userRepository.getForPinCode(auth.user.id);
this.validatePinCode(user, dto);
await this.userRepository.update(auth.user.id, { pinCode: null });
await this.sessionRepository.lockAll(auth.user.id);
}
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
const user = await this.userRepository.getForPinCode(auth.user.id);
this.validatePinCode(user, dto);
View on GitHub (pinned to f48d4b3321)