Один аккаунт, две двери: во что на самом деле обходится вход по телефону или почте
Разрешить людям входить хоть по номеру телефона, хоть по почте выглядит правкой на одну строку: расширяем поиск, принимаем оба варианта. Мы выкатили это на прошлой неделе в живом проекте, и правка на одну строку оказалась правкой модели данных — со счётом в придачу.
Вот во что она обходится на самом деле, в том порядке, в каком приходят счета.
Поиск никогда не был проблемой
Фреймворк хранил одну колонку userIdentifier и искал по строгому равенству.
Попроси его найти профиль по телефону или почте — и расширять просто негде: не
потому что запрос сложный, а потому что сама аутентификация требует, чтобы эта
колонка существовала. Убери её — и ядро падает на старте.
Так что первая правка — не «искать по двум полям». Это дать приложению право голоса в том, как находится пользователь:
DwAuthConfig(
findUserProfileByIdentifier: (session, identifier) async {
// приложение само решает, что значит «тот же самый человек»
},
);
Колонка перестаёт быть обязательной, идентификатор приходит уже нормализованным, а фреймворк перестаёт делать вид, что знает, что такое личность в вашей предметной области. Три обязанности переезжают в приложение, и документация говорит об этом прямым текстом — резолвер, который молча делает не то, хуже, чем отсутствие резолвера.
Поиск по двум полям сам по себе ничего не чинит
Вот эта часть нас и удивила, и ради неё написан этот пост.
У пользователя, который зарегистрировался по телефону, колонка с почтой пустая. Поиск по обоим полям не находит ничего нового, потому что во втором поле ничего нет. Поиск одновременно корректен и бесполезен.
Чтобы заполнить эту колонку, нужен способ привязать второй канал к существующему
аккаунту — а значит, addAuthProvider и changeIdentifier должны реально
работать. У нас оба бросали UnimplementedError, и экрана для этого не было ни в
одном макете. Пока его нет, вход с другого канала по-прежнему тихо создаёт второй
аккаунт, ровно как и раньше.
Дубли уже есть
Дальше идёт то, что отложить не получится. Мы пошли добавлять уникальные индексы на
phone и email и обнаружили, что уникальных индексов не было вообще — ни на
новых колонках, ни на старом userIdentifier. Две строки с одинаковым значением
уже были легальны, и поиск возвращал ту, которую базе захочется отдать.
Индекс не добавить, пока дубли не слиты. А дубли существуют именно потому что вход со второго канала раньше создавал новый аккаунт. Баг и его разгребание — один и тот же объект.
Слияние — это событие, а не действие
Слияние аккаунтов необратимо: один профиль поглощает другой, и после этого никто не
восстановит, что откуда пришло. Поэтому мы описали его как событие ProfileMerge,
а не как вызов метода — кто слил, кого и что переехало. Действие оставляет базу
консистентной, а историю потерянной; событие оставляет и то, и другое.
Что мы сказали бы себе неделю назад
- «Вход по телефону или почте» — задача не про экран авторизации. Это задача про идентичность профиля, а экран — последние пять процентов.
- Выкатывайте привязку второго канала в том же релизе. Без неё фича — это поиск, который ничего не находит.
- Проверяйте уникальные индексы до того, как что-то пообещали. У нас их не было за всю жизнь проекта, и никто не заметил, потому что ничто их не требовало.
- Проверяйте на живой базе полным кругом: регистрация по телефону, привязка почты, вход по почте в тот же аккаунт, одна строка в таблице. У нас 23 серверных теста, включая этот проход; версия этой статьи, написанная по чтению кода, была бы неверна в двух местах.
Со стороны фреймворка это приехало в ядро dartway 0.12.1 — резолвер аутентификации
и работающий changeIdentifier. Если вы на более ранней версии, интересная часть —
миграция, а не API.