0% прочитано

Чому я не змінюю електронну адресу одразу після збереження

Нова електронна адреса не повинна ставати основною лише тому, що її ввели у формі. Я залишаю стару адресу активною, створюю окремий запит на зміну і застосовую нову лише після підтвердження власності.

6 жовтня 2026 р. 9 хв читанняЛаравель

Зміна електронної адреси здається звичайним оновленням профілю.

Користувач вводить нову адресу, натискає кнопку збереження, а сервер змінює поле email.

Але електронна адреса часто є не просто текстом у профілі. Вона може використовуватися для входу, відновлення доступу, повідомлень і підтвердження особи.

Тому для мене введення нової адреси ще не означає, що користувач справді контролює її.

Поки нова адреса не підтверджена, стара залишається основною.

Натискання кнопки збереження створює намір змінити адресу. Підтвердження нової адреси завершує саму зміну.

Просте оновлення поля створює небезпечне вікно

PHP - Нова адреса застосована до підтвердження
$user->email = $request->email;$user->email_verified_at = null; $user->save(); $this->sendVerificationEmail(    $user,);

Після такого запису система вже вважає нову адресу адресою користувача.

Але володіння нею ще не доведене.

Стара адреса може перестати працювати для входу або відновлення доступу ще до того, як нова стала надійною точкою контакту.

Тобто система змінює важливу частину облікового запису раніше, ніж має достатньо доказів для цієї зміни.

Я зберігаю нову адресу як запит на зміну

Основна адреса не змінюється завчасно
Поточна адресакористувача old@example.com      |      | користувач просить зміну      vзапит на зміну new@example.com      |      | підтвердження      vнова основна адреса new@example.com

Я розділяю два стани.

Перший - підтверджена основна адреса облікового запису.

Другий - адреса, на яку користувач лише попросив перейти.

Поки друга адреса не підтверджена, вона не замінює першу і не повинна автоматично отримувати всі права основної адреси.

Запит на зміну має власний життєвий цикл

PHP - Спрощена модель запиту на зміну
Schema::create(    'email_change_requests',    function (Blueprint $table): void {        $table->id();         $table->foreignId('user_id');         $table->string(            'proposed_email',        );         $table->string(            'token_hash',        );         $table->timestamp(

Це лише спрощений приклад структури.

Для мене важлива сама ідея: очікувана зміна є окремим станом, а не наполовину зміненим користувачем.

Запит має власника, нову адресу, обмежений строк дії та інформацію про те, чи був він уже використаний.

Нову адресу я нормалізую до створення запиту

PHP - Одна форма адреси для порівняння
$email = mb_strtolower(    trim(        $request->string('email')->toString()    ));

Перевірка унікальності має працювати з тією ж формою адреси, яка використовується для збереження та порівняння.

Інакше одна частина системи може вважати дві адреси різними, а інша - однаковими.

Тому нормалізація для мене є частиною межі ще до створення запиту на зміну.

Першу перевірку унікальності я роблю одразу

PHP - Не створювати завідомо некоректний запит
if (    User::query()        ->where(            'email',            $email,        )        ->whereKeyNot(            $user->id,        )        ->exists()) {    throw ValidationException::withMessages([        'email' => 'This email is already in use.',    ]);}

Ця перевірка корисна для швидкого зворотного зв’язку користувачу.

Якщо адреса вже зайнята, немає сенсу відправляти лист підтвердження.

Але ця перевірка ще не є остаточною гарантією.

Секрет підтвердження я не зберігаю у відкритому вигляді

PHP - Зберігається лише похідне значення
$token = Str::random(64); $request = EmailChangeRequest::query()    ->create([        'user_id' =>            $user->id,         'proposed_email' =>            $email,         'token_hash' =>            hash(                'sha256',                $token,            ), 

Сирий токен потрібен лише для формування посилання підтвердження.

У базі я залишаю лише значення, з яким можна перевірити отриманий токен, але не відновити початковий секрет.

Інший варіант - підписане посилання з обмеженим строком дії. В обох випадках підтвердження має бути пов’язане з конкретним запитом і мати кінцевий строк життя.

Посилання на зміну не повинно працювати безстроково

Запит на зміну адреси відображає намір користувача в конкретний момент часу.

Я не хочу, щоб старе посилання через місяці несподівано змінило важливу частину облікового запису.

Тому кожен запит має expires_at, а прострочене підтвердження відхиляється.

Новий запит повинен робити старий неактуальним

Старе посилання не повинно перемогти новий намір
Запит 1 old@example.com-> first-new@example.com  Користувач передумав  Запит 2 old@example.com-> second-new@example.com  Запит 1

Якщо користувач кілька разів вводить різні адреси, старі посилання не повинні залишатися рівноправними.

Новий запит замінює попередній або попередній явно позначається неактуальним.

Інакше користувач може підтвердити старий лист уже після того, як вибрав іншу адресу.

Підтвердження перевіряє стан ще раз

PHP - Підтвердження працює з актуальним запитом
DB::transaction(    function () use (        $requestId,        $token,    ): void {        $change = EmailChangeRequest::query()            ->whereKey($requestId)            ->lockForUpdate()            ->firstOrFail();         if (            $change->consumed_at !== null            || $change->expires_at->isPast()        ) {            throw InvalidEmailChange::requestUnavailable();        }

У момент підтвердження я вже не покладаюся на те, що було правильним під час створення запиту.

Перевіряється сам запит: чи він ще діє, чи не був уже використаний, чи збігається підтвердження.

А потім повторно перевіряються умови, які могли змінитися за час очікування.

Унікальність потрібно перевірити ще раз саме під час підтвердження

Стан міг змінитися під час очікування
10:00 Користувач A просить:new@example.com Адреса вільна  10:05 Інший обліковий записотримує:new@example.com  10:10

Це один із найважливіших моментів у всьому сценарії.

Перевірка унікальності перед відправленням листа лише доводить, що адреса була вільна тоді.

Між створенням запиту і переходом за посиланням можуть пройти хвилини або години.

Тому перед фактичною зміною основної адреси я перевіряю унікальність ще раз.

Остаточну гарантію унікальності я залишаю базі даних

Навіть повторна перевірка в коді не закриває всі конкурентні сценарії.

Два запити можуть одночасно перевірити одну й ту саму адресу до того, як один із них виконає запис.

Тому унікальне обмеження в базі даних залишається останньою гарантією.

Код дає зрозумілу поведінку і хорошу помилку. База гарантує, що суперечливий стан фізично не буде збережений.

Саму зміну основної адреси я виконую атомарно

PHP - Адреса і стан запиту змінюються разом
$user = User::query()    ->whereKey(        $change->user_id,    )    ->lockForUpdate()    ->firstOrFail(); $emailIsTaken = User::query()    ->where(        'email',        $change->proposed_email,    )    ->whereKeyNot(        $user->id,    )    ->exists();

Після успішного підтвердження нова адреса вже доведена як контрольована користувачем.

Тому вона може стати основною і відразу вважатися підтвердженою.

У тій самій транзакції запит позначається використаним, щоб повторне відкриття того самого посилання не виконувало операцію вдруге.

До підтвердження стара адреса продовжує працювати

Це важлива властивість такого підходу.

Користувач не втрачає основну адресу лише через помилку під час введення нової.

Якщо лист не прийшов, адреса введена неправильно або користувач просто передумав, поточний обліковий запис залишається в нормальному стані.

Невдалий процес зміни не перетворюється на проблему доступу.

Повторне відкриття посилання не повинно повторювати зміну

Підтвердження має бути одноразовою операцією.

Після успішного використання запит переходить у завершений стан.

Наступний перехід за тим самим посиланням не повинен знову змінювати адресу, надсилати ті самі побічні дії або відновлювати старий запит.

Поточний сеанс не повинен випадково ламатися під час зміни

Зміна електронної адреси не повинна випадково залишити поточний сеанс у суперечливому стані.

Після успішного підтвердження дані автентифікованого користувача мають відповідати вже новій основній адресі.

Конкретна політика повторного входу залежить від вимог продукту, але вона повинна бути явною, а не випадковим наслідком зміни одного поля.

Підтвердження доводить контроль нової адреси, а не довіру до старого стану

Мені важливо не змішувати різні докази.

Те, що користувач уже увійшов у свій обліковий запис, доводить його доступ до поточного сеансу.

Але це не доводить володіння новою електронною адресою.

Посилання, доставлене на нову адресу, якраз і закриває цю окрему перевірку.

Повний потік зміни адреси

Від наміру до підтвердженої зміни
1. Користувач вводить нову адресу 2. Адреса нормалізується 3. Виконується рання   перевірка унікальності 4. Основна адреса   НЕ змінюється 5. Створюється запит   на зміну зі строком дії 6. На нову адресу   надсилається підтвердження 

Що я перевіряю тестами

Основні сценарії
1. Користувач вводить нову адресу   -> основна адреса не змінюється   -> створюється запит на зміну  2. Правильне підтвердження   -> нова адреса стає основною   -> адреса підтверджена   -> запит використаний  3. Прострочене підтвердження   -> адреса не змінюється  4. Неправильний токен

Що я перевіряю в механізмі зміни електронної адреси

Мій короткий список:

  • Чи залишається поточна адреса основною до підтвердження?
  • Чи зберігається нова адреса окремо як очікувана зміна?
  • Чи має запит обмежений строк дії?
  • Чи не зберігається секрет підтвердження у відкритому вигляді?
  • Чи робить новий запит попередній неактуальним?
  • Чи перевіряється унікальність і під час створення, і під час підтвердження?
  • Чи є унікальне обмеження в базі даних?
  • Чи виконується остаточна зміна атомарно?
  • Чи можна використати підтвердження лише один раз?
  • Чи не втрачає користувач доступ, якщо нову адресу так і не підтвердив?

Збереження форми і зміна облікового запису - це не одна подія

Для звичайного профільного поля натискання кнопки збереження часто справді означає негайну зміну.

Для електронної адреси цього недостатньо.

Спочатку користувач лише заявляє про намір перейти на нову адресу.

Система зберігає цей намір окремо, надсилає підтвердження, обмежує строк його дії і не чіпає поточну основну адресу.

Лише після підтвердження вона повторно перевіряє актуальний стан і атомарно завершує зміну.

Я не змінюю важливий ідентифікатор облікового запису раніше, ніж система отримала доказ контролю нової адреси.

Нова електронна адреса стає основною не після збереження форми, а після доказу, що користувач справді має до неї доступ.