immich-app/immich · warning · BadRequestException
User already has a PIN code
Error message
User already has a PIN code
What it means
NestJS BadRequestException (HTTP 400) thrown by AuthService.setupPinCode when the authenticated user already has a stored pinCode hash. Immich restricts PIN setup to a one-time operation; subsequent attempts are rejected so the existing PIN must be changed via PUT /auth/pin-code or reset via DELETE /auth/pin-code. The guard is a simple truthiness check on user.pinCode loaded by userRepository.getForPinCode.
Source
Thrown at server/src/services/auth.service.ts:155
const updatedUser = await this.userRepository.update(user.id, { password: hashedPassword });
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 199723261c)
Solutions
- If the user already has a PIN, call PUT /auth/pin-code (changePinCode) with {password|pinCode, newPinCode} instead of POST /auth/pin-code.
- Call GET /auth/status first and inspect the `pinCode` boolean to decide setup vs. change in the UI.
- To wipe and re-pin from scratch, call DELETE /auth/pin-code with the password, then POST /auth/pin-code.
- If the error is unexpected, query the user row's pinCode column to confirm state.
Example fix
// before
await api.authApi.setupPinCode({ pinCode: '123456' });
// after
const status = await api.authApi.getAuthStatus();
if (status.pinCode) {
await api.authApi.changePinCode({ pinCode: oldPin, newPinCode: '123456' });
} else {
await api.authApi.setupPinCode({ pinCode: '123456' });
} Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: avoid POST /auth/pin-code when a PIN already exists
async function canSetupPin(): Promise<boolean> {
const { pinCode } = await api.authApi.getAuthStatus();
return !pinCode;
}
if (!(await canSetupPin())) {
throw new Error('PIN already set; use changePinCode instead');
} Type guard
function hasPin(status: AuthStatusResponseDto): status is AuthStatusResponseDto & { pinCode: true } {
return status.pinCode === true;
} Try / catch
try {
await api.authApi.setupPinCode({ pinCode });
} catch (e) {
if (e.response?.status === 400 && e.response?.data?.message === 'User already has a PIN code') {
await api.authApi.changePinCode({ pinCode, newPinCode: pinCode });
} else {
throw e;
}
} Prevention
- Drive the setup vs. change decision from GET /auth/status.pinCode.
- Never assume the absence of a PIN; always re-fetch status before setup.
- Treat 400 'User already has a PIN code' as a signal to switch to changePinCode, not as a hard failure.
When it happens
Trigger: POST /auth/pin-code with body {pinCode} while the user row already has a non-null pinCode column. Happens when a client retries setup after a previous success, or when the mobile/web app calls setup instead of change after the first PIN is configured.
Common situations: Client UI flow bug that calls 'setup' on every PIN save instead of distinguishing first-time setup vs. update; race where two concurrent setup requests both pass the check; restored database that retained the old PIN.
Related errors
- User does not have a PIN code
- Either password or pinCode is required
- Wrong PIN code
- Forbidden
- Missing required permission: ${requestedPermission}
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/cb51d4ae03320a73.
Report an issue: GitHub.