О сервисе
- Название
- TSOY ID
- Адрес
- https://id.tsoy.my
- Версия движка
- 5.6.0
- PHP
- 8.4.27
Название, логотип, цвет и текст подвала настраиваются в разделе «Настройки» — движок не навязывает своего имени.
SSO — открытый OpenID-провайдер на PHP: один аккаунт на сеть сайтов, без Composer и Node. Лицензия MIT: можно использовать, дорабатывать и разворачивать у себя.
Правовые документы этой инсталляции. Редакция и дата изменения — на странице каждого.
5.6.0 · Мелочи
Остаток ревизии кода: мелкие находки, каждая — по замеру. Две из списка замерены и оставлены как есть: разбор CHANGELOG на «О сервисе» занимает 5 мс из 15, а счёт участников в API — единицы миллисекунд на установку; кэш ради них был бы вторым механизмом без пользы. IP за прокси тоже не трогали: боевой адрес смотрит прямо на сервер.
Безопасность
- Перебор адресов по времени входа. Ответ «нет такого аккаунта» приходил в 15 раз быстрее «неверного пароля» (19 мс против 320): пароль для несуществующего аккаунта не проверялся. Теперь проверяется по хэшу-пустышке с теми же параметрами.
- Пароль внутри сессии — при смене пароля и почты, удалении, выключении 2FA, слиянии — проверяется одним помощником с лимитом 10 попыток в час. Раньше укравший сессию подбирал пароль прямо из профиля без счёта.
- Пароли, PIN, секреты и коды восстановления помечены
#[\SensitiveParameter]и не попадают в трассировки ошибок. - Вход по ключу идёт той же дорогой, что остальные: «войти и привязать», вебхук
login.success. Ключ без проверки пользователя (без PIN и биометрии) больше не заменяет включённую 2FA — после него спрашивается код. - Архив бэкапа с ключами подписи удаляется, даже если скачивание оборвали; отметку «бэкап сделан» ставит только скачивание до конца.
- Админ портала видит в карточке человека только свой портал и выдачи, которые его касаются. Раньше там были все порталы и роли человека, его сессии и журнал по всей системе, а добавить в портал можно любого по почте. Сессии, журнал и активность человека — разделы глобального админа; в меню карточки их у админа портала больше нет.
- Журнал организации целиком — только тем, кому журнал выдан на саму организацию. Участник организации с журналом одного портала видел все её события.
Удобство
- Человек без пароля (соцвход, ссылка из письма) выключает 2FA текущим кодом из приложения. Раньше форма просила пароль, которого нет.
Надёжность
- Индексы
audit_log (ip, id)иsessions (expires_at): журнал по адресу и счёт живых сессий шли полным проходом по таблице (миграция 031). - Миграции — один прогон за раз: выкладка и кнопка в админке больше не применяют одну версию дважды.
Проверки
tests/small_bounds.php(17): время ответа, лимит пароля в профиле,SensitiveParameter, 2FA без пароля, временный архив, план запросов, замок миграций, вход по ключу с UV и без, карточка человека и журнал организации глазами админа портала; 12 мутаций.
5.5.0 · Антифрод
Ботов удаляли пачкой на «Похоже на ботов», и от них не оставалось ничего, на чём учиться. Аудит переживал аккаунт, но не помнил, что это был бот: удалённый бот и удалённый человек в журнале неотличимы. Журнал активности уходил вместе с аккаунтом.
Добавлено
- Архив образцов ботов. Перед удалением «как бота» откладывается обезличенный образец: подсеть (не адрес), строка браузера, домен почты, сработавшие признаки, оценка и поведение — сколько запросов, куда (номера в адресах заменены на
{n}), чем ответили, сколько секунд заполнялась форма регистрации. Ни ящика, ни имени, ни адреса, ни номера аккаунта: бот подставляет чужую настоящую почту. Срок — «Архив антифрода, дней» в политиках (по умолчанию 365, 0 — вечно). - Архив учит. Подсеть и браузер из архива — признаки: следующий бот оттуда отмечен к удалению сразу.
- Оценка риска 0–100 — сумма весов признаков, галочка от 40. Сильный признак (устаревший браузер, закрытый адрес, толпа с подсети, подсеть из архива) решает один; слабые — отказы ботам и подбор паролей с той же подсети, браузер из архива — только вместе.
- Раздел «Антифрод» — вкладка журналов (
/admin/fraud): плитки с кривыми за 30 дней, откуда идут, что сработало, чем притворяются, домены почты, образцы и выгрузка в JSON — каждая выгрузка в аудите.
Проверки
tests/fraud.php: веса и порог, шум с подсети, в образце нет почты, имени и адреса, поведение до и после регистрации, «архив учит», раздел, выгрузка, срок хранения. Десять мутаций — все пойманы.admin_botsубирает за собой образцы, иначе они становились признаком на следующем прогоне.
5.4.0 · Ревизия
Полная ревизия кода нашла дыры одной природы: проверка есть в одном пути и забыта в соседнем. Админка сверяла права — API и SCIM нет; вход паролем спрашивал второй фактор — соцвход нет; /authorize требовал согласия и 2FA портала — FedCM нет. Худшее: админ портала мог выписать себе приглашение с ролью суперадмина и принять его.
Безопасность
- Приглашение портала выдаёт только роль, которую портал знает (
RoleOffer), и только в области портала. Раньше номер роли из формы ни с чем не сверялся, а роль без портала выдавалась на всю систему. - Делегированный admin больше не делает ссылку входа и сброса для суперадмина и не вливает свой аккаунт в суперадмина; перенос настроек с секретами — только суперадмину; админ портала не меняет организацию-владельца и не перезапускает события чужого портала.
- Admin API:
roles:writeне выдаётsuperadmin,settings:writeне пишет владельца и ключи подписи; правка логина проверяет формат и занятость. - SCIM: удаление группы — полное (
Org::purge), токен*пускают, суперадмина не блокирует. - Блокировка — один механизм на админку, API и SCIM (
UserBlock): сессии с оповещением порталов, refresh-токены, события. - Соцвход спрашивает код 2FA и не связывается сам с аккаунтом, где почта не подтверждена (захват аккаунта до регистрации хозяина адреса).
- FedCM не выдаёт токен мимо обязательной 2FA, согласия, подтверждения почты и разрешённых scope; запертая сессия кода не выдаёт ни там, ни в
/authorize. - Refresh-токен не живёт после завершения своей сессии; чужой клиент его не «сжигает»;
auth_time— время входа, а не обновления. - Исходящие запросы — только http(s): адрес сервиса и приёмника событий проверяется при правке, curl больше не умеет
gopher://иdict://. - Журнал «Почта» показывает письмо без действующих ссылок и кодов: роль «только чтение» видела там ссылку сброса пароля владельца.
- Именное приглашение принимается только с подтверждённой почтой; «Это не я» переживает вторую смену почты; отмена удаления — кнопкой, а не переходом (почтовые сканеры); выгрузка — только своему; setup-ссылка и передача владения без гонок.
Надёжность
- Лимит частоты атомарен; cron не запускается поверх незавершённого; очереди почты, вебхуков и событий берут запись условным обновлением — без двойных писем.
- Сроки хранения чистят и неотправленные письма, доставки событий, счётчики лимитов; ждущее доставки не трогают.
- Картинка больше 25 Мп не разбирается (PNG в 24 КБ разворачивался в гигабайты и ронял запрос).
- Установщик закрыт, если конфиг есть, а
installed.lockпропал. - Markdown: ссылки
//host— внешние. Удалено 9 методов без вызовов.
Проверки
tests/access_bounds.php(20),tests/login_bounds.php(11),tests/infra_bounds.php(13); на прежнем коде краснели 15 и 7 первых двух.- 24 мутации, каждая ловится своей проверкой.
5.3.1 · Окно на портале
Владелец: окно входа на странице портала открывается, но «Регистрация» и «Не помню» превращают его в серый прямоугольник с грустным значком.
Замер: заголовки в порядке — /register и /recover порталу встраивать разрешено. Падает сам процесс фрейма. На той же версии Chrome (156, Windows) падение повторяется и без расширений, а без View Transitions — нет: 4.97 включила переход между вкладками и во фрейме, а document.startViewTransition() во фрейме с чужого сайта роняет его. На странице самого SSO переход работает.
Исправлено
- Вкладки в окне входа на портале больше не роняют окно. Во фрейме подмена идёт без View Transitions: новое содержимое проявляется обычной CSS-анимацией. Вне фрейма переходы прежние. Правка в app.js SSO — у всех порталов сразу, на их стороне ничего менять не нужно.
- Окно на странице портала не прыгает. Оно стояло по центру, и при смене вкладки (вход 830px, регистрация 400, «Не помню» 360) уезжало вверх-вниз на сотни пикселей. Теперь верхний край на месте, а высота меняется плавно. Во фрейме нет постоянной полосы прокрутки — светлой ленты вдоль правого края.
- «Не помню» с портала знает портал. Вкладка показывала знак установки вместо шапки портала, а после отправки теряла ключ возврата: «Вход» вёл на безымянный вход. Теперь шапка портала, ключ в форме и в возврате.
- Одна шапка на три вкладки. «Вход», «Регистрация» и «Не помню» показывают одни и те же два знака связки, меняется только подпись — шапка не прыгает при переключении.
Проверки
tests/frame.php(+8): шапка портала на «Регистрация» и «Не помню», ключ возврата в форме и после отправки, во фрейме нет View Transitions, окно с верхним краем на месте и плавной высотой, без полосы прокрутки во фрейме.- 7 мутаций, все пойманы.
- В Chrome 156 (Windows, с Adblock Plus и без): до правки фрейм падает на первой же вкладке, без перехода — живой на всех трёх. Во фрейме в Chromium пройден весь путь: вход → регистрация (два шага) → «Не помню» с отправкой → вход паролем → код уходит странице портала.
5.3.0 · История в кабинете
Кабинет показывал пять последних входов — и всё. Чужой вход неделю назад, перебор пароля, отключённый второй фактор человек мог увидеть только в письме, если письма включены; полная картина была только у админа в журнале.
Добавлено
- «История» в кабинете (
/profile/activity): входы со способом, устройством, адресом (той же маской, что в «Устройствах») и сервисом; неудачные попытки — красной строкой и словами; изменения защиты и то, что с аккаунтом делал админ. По дням местного времени («Сегодня», «Вчера»), фильтры «Всё / Входы / Неудачи», «Раньше» — по 50 событий. - Не попадает: обмен кода и обновление токена (идут на каждый заход в портал) и действия самого человека в админке — это история установки, а не его.
- Три и больше неудачных попыток за неделю — предупреждение сверху с кнопками «Сменить пароль» и «Устройства».
- Ссылка «Вся история →» в карточке «Последние входы» профиля и ярлык «История» у установленного приложения.
Проверки
tests/profile_history.php(22): состав истории, фильтры, продолжение без повторов, неудачи за неделю, местные сутки, экран; 9 мутаций.
5.2.0 · Аналитика портала и системы
Карточка портала отвечала «всё ли в порядке сейчас», но не «как идёт»: ни входов по дням, ни активных за неделю и месяц, ни того, сколько событий портал не принял и почему. Скорость и ошибки приложения лежали в журнале запросов, и сложить их можно было только руками.
Добавлено
- Раздел «Аналитика» в карточке портала (группа «Наблюдение»), с тем же переключателем периода: входы в сервис и разница к прошлому периоду, график по дням, активные люди за сегодня, неделю и 30 дней, обновления токенов и повторы refresh-токена, доля доставленных событий (вниз — 99,6 % не превращается в 100 %), чем портал не принял события, запросы доступа и отказы.
- Вкладка «Система» рядом с «Пультом» и «Аналитикой» (только глобальному админу): запросы по дням без стука сканеров, скорость ответа корзинами («95 % запросов — до 200 мс»), ошибки сервера (5xx) — второй график только если они были, самые медленные адреса (от пяти запросов на адрес), почта: отправлено, в очереди, не ушло.
- Скорость считается корзинами одним проходом по журналу, без сортировки всех строк периода: за 90 дней это сотни тысяч строк.
Изменено
- Переключатель периода и разница к прошлому периоду — общие помощники
period_switch()иkpi_delta(): пульт, аналитика и карточка портала рисуют их одинаково.
Проверки
tests/analytics_portal.php(27): входы портала и прошлый период, окна активных, токены только своего портала, доставка и её ошибки, отказы; корзины скорости, процентили, стук сканеров не считается, единичный долгий запрос не попадает в медленные; экраны и права; 10 мутаций.
5.1.0 · Аналитика входа и регистрации
Пульт показывал вход и регистрацию одной цифрой. Сколько людей бросило регистрацию на подтверждении почты, в какие часы нагрузка, почему не входят и с каких браузеров — нигде не было: шаги регистрации не писались вовсе, а причина неудачи лежала в meta, и собрать её можно было только глазами по журналу.
Добавлено
- Вкладка «Аналитика» рядом с «Пультом» (
/admin/analytics, только глобальному админу), с тем же переключателем периода; период переезжает между вкладками. - Регистрация по шагам: ввели почту → подтвердили → завершили, с долей от предыдущего шага. Для этого ввод почты и её подтверждение (кодом и ссылкой) пишутся в аудит событиями
signup.startedиsignup.verified— без адреса. До 5.1 шагов в журнале нет, и экран так и говорит, а не рисует ноль. - Когда входят: тепловая карта «день недели × час» по местному времени; для «сегодня» — за неделю. Один оттенок, пять ступеней от самого людного часа; рядом — таблица.
- Почему не вошли: причины неудачных входов (нет пользователя, неверный пароль, незнакомый или не прошедший проверку passkey), неизвестная причина не теряется.
- Способы входа долями, браузеры и системы — по той же подписи устройства, что в сессиях; длинный хвост свёрнут в «Другие».
Проверки
tests/analytics.php(24): воронка, местные часы недели (вход после полуночи по местному — уже следующий день), причины, браузеры, маршрут и права, экран; 10 мутаций.
5.0.0 · Приложение
Вектор владельца — SSO как лёгкое приложение. В 4.97 без перезагрузки ходили только вкладки входа; кабинет и админка перезагружали страницу на каждом щелчке: островок, док и подвал рисовались заново, фильтр в журнале мигал всем экраном.
Изменено
- Кабинет и админка без перезагрузки. Переход между экранами с островком подменяет только содержимое — тем же механизмом, что вкладки входа (второго пути нет). Островок и док остаются на месте и отмечают новый раздел. Адрес настоящий, «назад» и «вперёд» работают. Вглубь экран уезжает вперёд, наверх — назад; при «меньше движения» — мгновенно.
- Раздел подгружается заранее — при наведении или касании.
- Фильтры, поиск и период на пульте — тоже без перезагрузки: выбор в фильтре журнала сразу меняет список.
- Командная палитра (Ctrl+K) открывает раздел той же подменой.
- Обычным переходом идут: формы с отправкой данных (проверяет сервер), файлы и выгрузки, GET с действием (отмена, язык, прозвон здоровья), ссылки с якорем, редактор экрана входа. Такие адреса и не подгружаются заранее — наведение не запустит выгрузку.
- Всё, что настраивалось при загрузке страницы (тосты, QR, фильтры на телефоне, маски, клетки под цифры, матрица прав, отбор строк, редактор обложки, «?do=»), настраивается и на подменённом экране.
Исправлено
Находки ревизии скриптов:
- Вход во фрейме: высота окна застывала на первом экране — мерилась отсоединённая после подмены обёртка.
- «Назад» дважды подряд во время перехода менял адрес, а экран оставался прежним.
- Тосты, пришедшие после подмены, не закрывались.
- Удаление своей схемы оформления молчало при отказе сервера и сбое сети.
- Поиск в палитре: запоздавший ответ затирал свежий; ошибка сервера выглядела как «ничего не нашлось»; четыре строки не переводились.
- Passkey: ответ-страница вместо JSON давал сбой разбора вместо понятной ошибки.
- Иконки в скриптах шли по адресу спрайта без версии, и воркер мог держать прежний спрайт после выпуска.
- Убраны около сорока правил оформления, которые не применялись ни на одном экране.
Проверки
tests/app_swap.php: какие адреса подменяются (регулярки из скрипта на живых адресах), что идёт обычным переходом, что меняется при подмене, что настраивается заново, находки ревизии. Восемь мутаций.
4.99.0 · Пульт
Дашборд оставался таким, каким был до переработки остальных экранов: числа «сегодня» над кривыми «за 14 дней», один график регистраций без осей, на котором единственный всплеск растягивался на всю высоту, и здоровье списком из шести строк, где красная терялась среди зелёных. На телефоне первый экран целиком занимали «Первые шаги», до цифр нужно было листать.
Изменено
- Период: сегодня (по часам), 7, 14, 30 и 90 дней. Ссылками — адрес можно отправить, «назад» работает. Числа и кривые плиток — за один и тот же срок, с разницей к прошлому такому же периоду: рост входов зелёный, рост неудачных — красный.
- Входы по способам: столбцы с накоплением — пароль, ссылка из письма, passkey, соцсети. Круглая шкала, сетка, подписи дат; подсказка с числами по наведению и касанию; таблица-двойник для чтения с экрана. Цвета рядов — свои токены (
--series-1…4), проверенные на различимость при нарушениях цветового зрения; акцент установки их не перекрашивает. - «Требует внимания» вместо списка здоровья: только то, что не в порядке, со ссылкой, куда идти, — плюс всплеск неудачных входов (втрое выше обычного и не меньше 20), письма в очереди дольше 15 минут, события, которые порталы не приняли. Когда всё хорошо — одна строка «Всё в порядке».
- Порталы по входам за период — по первым входам в портал из сессии.
- Защита аккаунтов: доля людей с passkey или вторым фактором. Только число — ничего не требует.
- Первые шаги свёрнуты в одну строку с полосой прогресса; открыты, пока не сделано ничего.
- Телефон: плитки по две в ряд, график листается вбок вместо нечитаемых подписей. Числа досчитывают до значения, столбцы растут, кольцо дорисовывается — один раз; при «меньше движения» всё сразу на месте.
Проверки
tests/dashboard_stats.phpдополнен: период и границы, «сегодня» по местным часам, «Требует внимания» без зелёного и со всплеском, круглая шкала, экран целиком (период, свёрнутые шаги, цвет разницы). Шесть новых мутаций.
4.98.0 · Честные цифры
Разбор дашборда перед его переработкой: цифры в нём расходились с /metrics и с профилем. «Вход» был определён в коде трижды: дашборд считал пароль и ссылку из письма, /metrics — пароль, passkey и соцсети, профиль — как дашборд. Человек, вошедший по passkey или через соцсеть, на дашборде не появлялся, а в «Последних входах» профиля его вход не показывался. Сутки резались по UTC, а показывались по поясу установки: для UTC+3 «сегодня» начиналось в 03:00, и ночные входы уходили во вчера.
Исправлено
- Один «вход» на всю установку.
App\Core\Statsзнает, какие события считаются входом (пароль, ссылка из письма, passkey, соцсети), и дашборд,/metricsи профиль берут определение оттуда. - Сутки местные. «Сегодня», «за 7 дней» и ряды по дням на дашборде считаются по поясу, в котором человек читает даты (его собственный или пояс установки), а не по UTC.
- «Сейчас в сети»: подпись говорит и про число (живые сессии), и про кривую (новые сессии за день) — раньше кривая читалась как «онлайн по дням».
- В аудите подписаны «Установка завершена» и «Вход через внешний сервис» — раньше там стояли машинные имена.
Проверки
tests/dashboard_stats.php: каждый способ входа попадает в счёт, неудачный нет; три места считают черезStats; местная полночь для UTC+3; ночной вход ложится в местный день. Шесть мутаций ломают это по очереди.
4.97.0 · Как приложение
Владелец: «между вход / регистрация / забыл пароль не должно быть перезагрузки — плавные переходы, как в приложении». И: на портале, открытом по алиасу, окно входа поверх сайта показывало «сайт не позволяет установить соединение».
Замер на живом портале: на алиасе набор отдаёт адрес возврата на основной адрес. Окно SSO встраивается только адресами портала из его карточки — алиаса там нет, и браузер окно не показывает. Но и пустив его, вход бы не завершился: сессия входа (state, verifier) осталась в cookie алиаса, а возврат приходит на основной адрес.
Изменено
- Вкладки «Вход», «Регистрация», «Не помню» — без перезагрузки. Переход подменяет только колонку формы: панель с обложкой и видео не начинается заново. Адрес настоящий и в истории — «назад» возвращает вкладку. Вкладка подгружается заранее, при наведении или касании: к щелчку ответ уже здесь. Это тот же механизм, что ведёт вход во фрейме с 4.75, — второго пути нет.
- Переход плавный: плашка вкладки переезжает, содержимое уходит в сторону выбранной вкладки, высота колонки меняется без рывка (View Transitions). Браузер без поддержки или «меньше движения» в системе — мгновенно. Во фрейме на странице портала подмена тоже с переходом, а не скачком.
- Формы отправляются как раньше: проверяет сервер.
Исправлено
- Вход окном на алиасе портала. sso.js видит, что адрес возврата на другом адресе, и вместо пустого окна переводит на ту же страницу основного адреса — вход начинается там сам, без второго щелчка. Вход, начатый без щелчка, идёт страницей: отдельное окно браузер без щелчка не откроет.
- Набор портала знает алиасы:
'aliases' => ['www.example.com']вconfig.php— на алиасе возврат на тот же алиас тем же путём. Добавьте адрес возврата на алиасе в карточку портала — и окно входа пускается на алиасе, а человек остаётся там, где был. - Подпись активной вкладки пропадала под плашкой на время перехода между экранами входа: плашка рисовалась слоем поверх подписей. Подписи теперь своими слоями — над ней, как и на странице.
Проверки
tests/login_swap.php(15 проверок): только три вкладки, только колонка формы, история, View Transitions, подгрузка заранее, слои подписей; sso.js на алиасе; набор с алиасом — класс из шаблона, адрес возврата по хосту.- Проверено в Chromium: документ один на все переходы, «назад» работает, ошибок нет; у знака, вкладок, плашки и колонки — старый и новый снимки.
- 6 мутаций, все пойманы; три прежние мутации подмены во фрейме — на новом коде.
4.96.0 · Сначала почта
Регистрация была одной формой из четырёх полей: аккаунт заводился сразу, а почту подтверждали потом. Боты заполняли её чужими адресами, и в базе копились аккаунты, которых никто никогда не подтвердит. Владелец просил: сначала почта, потом код или ссылка из письма, и только потом имя и аккаунт.
Изменено
- Регистрация по шагам: почта → код → имя и пароль. Аккаунт рождается последним шагом и сразу с подтверждённой почтой — экрана «подтвердите почту» после регистрации больше нет. До последнего шага есть только запрос в сессии и одноразовый код в письме: бот, заполнивший первое поле, аккаунта не получает. Номер шага виден («Шаг 2 из 3»).
- В письме — и код, и кнопка. Кнопка продолжает регистрацию в том браузере, где её нажали, — хоть в другом; открытие ссылки само по себе ничего не делает (почтовые сканеры её не погасят). Ссылка одноразовая.
- Занятый адрес выглядит так же, как свободный: тот же экран кода, а владельцу адреса — письмо «кто-то пытался зарегистрироваться». Проверить, есть ли у нас человек, регистрацией нельзя.
- «Другой адрес» на шаге с кодом возвращает к первому шагу с прежним адресом в поле — поправить опечатку. «Прислать код ещё раз» — там же.
- Подтверждение почты выключено — шагов два: почта, затем имя и пароль; аккаунт неподтверждённый, письмо со ссылкой уходит, как раньше. Механизм один, в нём просто нет шага с кодом.
- Приглашение доживает до последнего шага, даже если ссылку из письма открыли в другом браузере, и гаснет на нём.
Исправлено
- «Капча → Требовать при регистрации» и «при восстановлении» не делали ничего. Переключатели были в настройках с 4.0, но ни регистрация, ни восстановление их не спрашивали. Теперь проверка стоит на первом шаге регистрации и на экране восстановления — со своим объяснением, а не с «появилась после неудачных попыток».
- Письма в журнале почты (драйвер log) шли в случайном порядке, если их отправили в одну секунду: в имени файла были секунды и случайный хвост. Теперь в имени микросекунды, и порядок имён — порядок отправки.
Проверки
tests/signup_steps.php(30 проверок, перезапускается): шаги, неверный и верный код, ссылка в другом браузере по кнопке, занятый адрес, «Другой адрес», два шага без подтверждения, обе капчи, приглашение до конца.- Регистрация в батареях — одним помощником
signup()вtests/lib.php; экран «подтвердите почту» вguard.phpпроверяется на аккаунте, который ждёт подтверждения с прежних версий. - 9 мутаций, все пойманы.
- Снимки 1440 и 390, светлая и тёмная: длинный адрес на третьем шаге распирал колонку до 448px на телефоне — исправлено.



