{"record":{"id":"018a99af37d39781","repo":"bytebase/bytebase","slug":"password-was-reset-but-failed-to-clear-the-login","errorCode":null,"errorMessage":"password was reset, but failed to clear the login attempt counter","messagePattern":"password was reset, but failed to clear the login attempt counter","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/component/recovery/service.go","lineNumber":377,"sourceCode":"\n\tresult := &ResetUserPasswordResult{WorkspaceID: request.WorkspaceID, Email: email}\n\tif bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), request.Password) != nil {\n\t\tpasswordHash, err := bcrypt.GenerateFromPassword(request.Password, bcrypt.DefaultCost)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to hash user password\")\n\t\t}\n\t\tif _, err := s.store.UpdateUser(ctx, user, &store.UpdateUserMessage{PasswordHash: new(string(passwordHash))}); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to reset user password\")\n\t\t}\n\t\tresult.Changed = true\n\t}\n\n\t// A lockout the user guessed themselves into must not outlive the reset:\n\t// without this, the operator hands over a password that Login keeps\n\t// refusing with ResourceExhausted until the window lapses.\n\tif err := s.store.ClearLoginAttempt(ctx, email, storepb.LoginAttemptKind_PASSWORD); err != nil {\n\t\tif result.Changed {\n\t\t\treturn result, errors.Wrap(err, \"password was reset, but failed to clear the login attempt counter\")\n\t\t}\n\t\treturn result, errors.Wrap(err, \"failed to clear the login attempt counter\")\n\t}\n\n\tif err := s.createAuditLog(ctx, request.WorkspaceID, resetUserPasswordAuditMethod, string(auditRequest)); err != nil {\n\t\treturn result, errors.Wrap(err, \"user password reset completed, but failed to create the recovery audit log\")\n\t}\n\treturn result, nil\n}\n\nfunc (s *Service) getActiveEndUser(ctx context.Context, email string) (*store.UserMessage, error) {\n\taccount, err := s.store.GetAccountByEmail(ctx, email)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to find user identity\")\n\t}\n\tif account == nil {\n\t\treturn nil, errors.Errorf(\"user %q does not exist\", email)\n\t}","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/bytebase/bytebase/blob/1870550677fe08f0d2a78c07acd27541464eb945/backend/component/recovery/service.go#L359-L395","documentation":"After (optionally) changing the password, ResetUserPassword clears the user's failed-login counter via store.ClearLoginAttempt so an existing lockout does not keep Login returning ResourceExhausted with the new password. If that clear fails AND the password was actually changed (result.Changed=true), the result is returned together with this error: the reset succeeded but the lockout persists.","triggerScenarios":"Calling ResetUserPassword for a user whose password was updated when ClearLoginAttempt fails: DB write failure on the login-attempt table, connection drop between UpdateUser and ClearLoginAttempt, or a missing login-attempt table/index from incomplete migrations.","commonSituations":"User was locked out from repeated wrong passwords and the operator resets them during a DB instability; partial migration left the login-attempt storage inconsistent; transient network failure between the two store calls.","solutions":["Inspect the wrapped inner error and retry ResetUserPassword — it is safe because the bcrypt compare makes the second run a no-op for the password, and it will re-attempt ClearLoginAttempt.","Manually clear the login-attempt record for the email (PASSWORD kind) if the API keeps retry-failing.","Verify the login-attempt table exists and is writable (migrations, privileges).","Inform the operator that the new password may still be refused with ResourceExhausted until the lockout window lapses if the counter cannot be cleared."],"exampleFix":"// before\nres, err := svc.ResetUserPassword(ctx, req)\nif err != nil { return err } // password changed but lockout still active\n// after\nres, err := svc.ResetUserPassword(ctx, req)\nif err != nil {\n    if res != nil && res.Changed {\n        // password WAS reset; retry just the counter clear / inform operator\n        log.Printf(\"password reset ok, clearing lockout failed: %v\", err)\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// check DB writability of both user and login-attempt storage first\nif err := store.Ping(ctx); err != nil {\n    return fmt.Errorf(\"metadata DB unstable, partial reset likely: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"res, err := svc.ResetUserPassword(ctx, req)\nif err != nil {\n    if res != nil && res.Changed {\n        // password was reset; lockout may persist — retry to clear the counter\n        res, err = svc.ResetUserPassword(ctx, req)\n    }\n    if err != nil { return err }\n}","preventionTips":["Retry the whole call on this error; bcrypt compare makes the re-run safe","Verify login-attempt storage is migrated and writable","Watch for ResourceExhausted on Login after resets to detect lingering lockouts"],"tags":["database","lockout","partial-failure"],"backgroundTag":"database-write-failed","analyzedSha":"1870550677fe08f0d2a78c07acd27541464eb945","analyzedAt":"2026-09-06T21:16:13.665Z","contentChangedAt":"2026-09-06T21:16:13.665Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}