immich-app/immich · error · BadRequestException

User does not have a PIN code

Error message

User does not have a PIN code

What it means

validatePinCode is the shared gate for resetPinCode, changePinCode, and unlockSession. It first requires that the user actually has a PIN configured; if the stored pinCode is null, all PIN operations are invalid and it throws a 400 'User does not have a PIN code', since there is nothing to verify against.

Solutions

  1. Call the PIN setup endpoint first (setupPinCode) to create a PIN, then retry the operation.
  2. Check PIN status via GET /api/auth/pin-code/status before invoking any PIN endpoint.
  3. Use the password-based flow (e.g. password login) instead of PIN unlock when no PIN exists.

Example fix

// before
await api.authenticationApi.unlockSession({ pinCode }); // no PIN set
// after
const { hasPin } = await api.authenticationApi.getPinCodeStatus();
if (!hasPin) await api.authenticationApi.setupPinCode({ pinCode });
await api.authenticationApi.unlockSession({ pinCode });
Defensive patterns

Strategy: validation

Validate before calling

const status = await api.authenticationApi.getPinCodeStatus();
if (!status.hasPin) {
  throw new Error('No PIN configured; set one up first or use password auth');
}

Try / catch

try {
  await api.authenticationApi.unlockSession({ pinCode });
} catch (e) {
  if (e.status === 400 && e.message === 'User does not have a PIN code') {
    // route user to PIN setup
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling resetPinCode, changePinCode, or unlockSession with pinCode/password credentials for a user account that has never set up a PIN (stored pinCode is null).

Common situations: Client UI showing PIN entry before the user completed PIN setup; calling unlockSession after another device reset the PIN; race where the PIN was cleared between listing and calling.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15). Data as JSON: /api/errors/5c703bc3221e70f2. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/auth.service.ts:187

    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);

    const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
    await this.userRepository.update(auth.user.id, { pinCode: hashed });
  }

  private validatePinCode(
    user: { pinCode: string | null; password: string | null },
    dto: { pinCode?: string; password?: string },
  ) {
    if (!user.pinCode) {
      throw new BadRequestException('User does not have a PIN code');
    }

    if (dto.password) {
      if (!this.validateSecret(dto.password, user.password)) {
        throw new BadRequestException('Wrong password');
      }
    } else if (dto.pinCode) {
      if (!this.validateSecret(dto.pinCode, user.pinCode)) {
        throw new BadRequestException('Wrong PIN code');
      }
    } else {
      throw new BadRequestException('Either password or pinCode is required');
    }
  }

  async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> {
    const admin = await this.createUser({
      isAdmin: true,

View on GitHub (pinned to f48d4b3321)