«Просить логін після кожного оновлення» — дві поломки в одному симптомі. Перша: RotateSession читав email::text без COALESCE, а пошта стала необов'язковою разом із входом за логіном. Користувач без адреси міг увійти й не міг поновити сесію взагалі. Друга: ротація не переживала подвійного обміну. React у режимі розробки виконує ефекти двічі, дві вкладки в проді дають ту саму гонку: перший запит відкликав токен, другий приносив уже відкликаний. Тепер core.sessions.replaced_by тримає ланцюг, вікно 30 секунд — вистачає на гонку від того самого клієнта й замало для реального повтору. PageBody один на всіх сторінках: відступи й прокрутка більше не залежать від того, хто писав сторінку. Заміряно — рівно 16 px скрізь. Мапа: видалення (ендпоїнт існував із Етапу 4, кнопки не було) з підтвердженням, і вигляд вузла — форма (картка/пігулка/крапка), товщина рамки, заливка, приховування підпису й цифр. Аварія й попередження світяться ореолом: рамка іншого кольору не помітна периферійним зором. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
21 lines
1.4 KiB
SQL
21 lines
1.4 KiB
SQL
-- =====================================================================
|
||
-- NetPulse :: 0019_session_rotation_grace.sql
|
||
-- Ротація refresh-токена перестає ламати сесію при подвійному обміні.
|
||
--
|
||
-- Симптом, з якого почалося: сторінка просила логін після кожного
|
||
-- перезавантаження. React у режимі розробки виконує ефекти двічі, тож
|
||
-- відновлення сесії йшло двома запитами поспіль. Перший обмінював
|
||
-- токен і відкликав старий, другий приносив уже відкликаний — і
|
||
-- отримував «сесії немає».
|
||
--
|
||
-- Це не лише про режим розробки: дві вкладки, відкриті одночасно,
|
||
-- дають рівно ту саму гонку в проді.
|
||
-- =====================================================================
|
||
|
||
-- На що замінили токен. Дозволяє простежити ланцюг: пред'явлений
|
||
-- щойно відкликаний токен веде до свого наступника, а не в нікуди.
|
||
ALTER TABLE core.sessions
|
||
ADD COLUMN replaced_by uuid REFERENCES core.sessions(id) ON DELETE SET NULL;
|
||
|
||
CREATE INDEX sessions_replaced_idx ON core.sessions (replaced_by)
|
||
WHERE replaced_by IS NOT NULL;
|