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

  1. If the user already has a PIN, call PUT /auth/pin-code (changePinCode) with {password|pinCode, newPinCode} instead of POST /auth/pin-code.
  2. Call GET /auth/status first and inspect the `pinCode` boolean to decide setup vs. change in the UI.
  3. To wipe and re-pin from scratch, call DELETE /auth/pin-code with the password, then POST /auth/pin-code.
  4. 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

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


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