{"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":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":159,"sourceCode":"    });\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":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/auth.service.ts#L141-L177","documentation":"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.","triggerScenarios":"Calling POST /api/auth/pin-code/setup for a user whose record already has a non-null pinCode hash.","commonSituations":"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.","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."],"exampleFix":"// before\nawait api.authenticationApi.setupPinCode({ pinCode }); // PIN already set\n// after\nconst { hasPin } = await api.authenticationApi.getPinCodeStatus();\nif (hasPin) {\n  await api.authenticationApi.changePinCode({ pinCode: currentPin, newPinCode });\n} else {\n  await api.authenticationApi.setupPinCode({ pinCode });\n}","handlingStrategy":"validation","validationCode":"const status = await api.authenticationApi.getPinCodeStatus();\nif (status.hasPin) {\n  throw new Error('PIN already set; use changePinCode or resetPinCode instead of setup');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.authenticationApi.setupPinCode({ pinCode });\n} catch (e) {\n  if (e.status === 400 && e.message === 'User already has a PIN code') {\n    // fall back to change/reset flow\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["pin-code","conflict","validation"],"backgroundTag":"file-already-exists","analyzedSha":"f48d4b332127ad365ba256108799ca8f571d2dd5","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}