
Сброс обычно пишут одним вызовом: отправить письмо, поменять хеш, вернуть 200. В разборе на dev.to предлагают считать письмо сообщением очереди, а не событием интерфейса, и разложить сброс на три перехода с записью в аудит.
Заявка создаёт запись с коротким сроком жизни: хеш токена, время истечения, статус pending. Подтверждение в одной транзакции сверяет хеш и срок и переводит запись в consumed ровно один раз. И только потом меняется пароль и запускается отзыв старых сессий. Без третьего шага укравший куку переживает смену пароля.
Ответ на заявку одинаковый для существующего и несуществующего адреса, иначе форма работает как проверялка почты.
Дальше в статье развилка: своя машина состояний на Go или managed-провайдер. Мне ценнее оказались сами инварианты: приложите их к своей реализации и увидите, какого перехода не хватает.