{"record":{"id":"cb51d4ae03320a73","repo":"immich-app/immich","slug":"user-already-has-a-pin-code","errorCode":null,"errorMessage":"User already has a PIN code","messagePattern":"User already has a PIN code","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/auth.service.ts","lineNumber":155,"sourceCode":"    const updatedUser = await this.userRepository.update(user.id, { password: hashedPassword });\n\n    await this.eventRepository.emit('AuthChangePassword', {\n      userId: user.id,\n      currentSessionId: auth.session?.id,\n      invalidateSessions: dto.invalidateSessions,\n    });\n\n    return mapUserAdmin(updatedUser);\n  }\n\n  async setupPinCode(auth: AuthDto, { pinCode }: PinCodeSetupDto) {\n    const user = await this.userRepository.getForPinCode(auth.user.id);\n    if (!user) {\n      throw new UnauthorizedException();\n    }\n\n    if (user.pinCode) {\n      throw new BadRequestException('User already has a PIN code');\n    }\n\n    const hashed = await this.cryptoRepository.hashBcrypt(pinCode, SALT_ROUNDS);\n    await this.userRepository.update(auth.user.id, { pinCode: hashed });\n  }\n\n  async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {\n    const user = await this.userRepository.getForPinCode(auth.user.id);\n    this.validatePinCode(user, dto);\n\n    await this.userRepository.update(auth.user.id, { pinCode: null });\n    await this.sessionRepository.lockAll(auth.user.id);\n  }\n\n  async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {\n    const user = await this.userRepository.getForPinCode(auth.user.id);\n    this.validatePinCode(user, dto);\n","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/auth.service.ts#L137-L173","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait api.authApi.setupPinCode({ pinCode: '123456' });\n\n// after\nconst status = await api.authApi.getAuthStatus();\nif (status.pinCode) {\n  await api.authApi.changePinCode({ pinCode: oldPin, newPinCode: '123456' });\n} else {\n  await api.authApi.setupPinCode({ pinCode: '123456' });\n}","handlingStrategy":"validation","validationCode":"// Pre-flight: avoid POST /auth/pin-code when a PIN already exists\nasync function canSetupPin(): Promise<boolean> {\n  const { pinCode } = await api.authApi.getAuthStatus();\n  return !pinCode;\n}\nif (!(await canSetupPin())) {\n  throw new Error('PIN already set; use changePinCode instead');\n}","typeGuard":"function hasPin(status: AuthStatusResponseDto): status is AuthStatusResponseDto & { pinCode: true } {\n  return status.pinCode === true;\n}","tryCatchPattern":"try {\n  await api.authApi.setupPinCode({ pinCode });\n} catch (e) {\n  if (e.response?.status === 400 && e.response?.data?.message === 'User already has a PIN code') {\n    await api.authApi.changePinCode({ pinCode, newPinCode: pinCode });\n  } else {\n    throw e;\n  }\n}","preventionTips":["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."],"tags":["auth","pin-code","nestjs","immich","validation"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}