Commit Graph

137 Commits

Author SHA1 Message Date
grendervill ecd52c11d3 feat(voice): вход в голосовые комнаты, voice states и модерация (Фаза 3)
AGENT.md 7.14, начало Фазы 3:

- `internal/voice`: собственный подписыватель токенов LiveKit (HS256, grants
  roomJoin/canPublish/canSubscribe/canPublishData) и адрес комнаты
  `guild_{g}_channel_{c}` — без внешних зависимостей;
- таблица `voice_states` (миграция 00009) и store-методы: вход, флаги
  (микрофон, звук, камера, экран), серверный мьют и глушение, перемещение,
  выход, подсчёт участников комнаты;
- ручки: POST /channels/{id}/voice/join (проверяет CONNECT_VOICE, тип комнаты,
  лимит участников и выдаёт токен), POST /channels/{id}/voice/leave,
  PATCH /guilds/{id}/voice-states/@me, GET /guilds/{id}/voice-states,
  PATCH /guilds/{id}/voice-states/{user_id} (MUTE_MEMBERS/DEAFEN_MEMBERS,
  аудит), POST /guilds/{id}/voice-states/{user_id}/move (MOVE_MEMBERS);
- события VOICE_STATE_UPDATE всем участникам сервера, системные записи о входе
  в голосовую комнату, READY отдаёт голосовые состояния серверов;
- конфиг: LIVEKIT_API_KEY/SECRET/URL приложению, публичный адрес
  `LIVEKIT_PUBLIC_URL` (по умолчанию ws(s)://<домен>/rtc) в установщике и
  compose; `/api/v1/meta` сообщает voice_enabled и voice_url;
- тесты: claims токена LiveKit и подпись, выключенный голос, имя комнаты, вход
  и флаги, запрет мьюта без прав, перемещение и выход.
2026-09-20 18:04:59 +03:00
grendervill 435c4deb9c feat(emojis): кастомные эмодзи сервера и системные сообщения
AGENT.md 7.6, 7.12:

- таблица `guild_emojis` (миграция 00008), загрузка PNG/JPEG/WebP/GIF до 512 КБ
  с правом MANAGE_EMOJIS, имя `[a-zA-Z0-9_]{2,32}`, лимит 100 на сервер;
- ручки: список эмодзи сервера, переименование, удаление (с удалением файла),
  загрузка multipart; аудит emoji.create/update/delete и событие
  GUILD_EMOJIS_UPDATE всем участникам сервера;
- кастомные эмодзи в READY (`guilds[].emojis` с токеном `<:name:id>`), реакции
  кастомными эмодзи проверяются по файлу, неизвестный эмодзи — 422;
- файлы эмодзи доступны авторизованным (используются в чужих сообщениях);
- системные сообщения: создание комнаты, вход участника (по приглашению и в
  публичный сервер) и исключение участника пишутся в ленту типом `system`;
- тесты: права на загрузку, список, реакция кастомным эмодзи, переименование и
  удаление, фильтрация системных записей в проверках истории.
2026-09-20 18:01:54 +03:00
grendervill 389fdd5f9a fix(web): панели не уезжают за экран (правка после замечания линтера)
Позиционирование переведено на прямые стили и класс `gl-anchored-panel`:
React не управляет left/top/max-height, поэтому повторный рендер не сбрасывает
раскладку, а `data-placement` задаёт направление анимации (панель может
развернуться вниз, если сверху мало места). Линтер по хукам чистый, 289 тестов
зелёные.
2026-09-20 14:36:32 +03:00
grendervill 494589ffca fix(web): крупные стрелки устройств и панели, не уезжающие за экран
- стрелки настроек микрофона, камеры и наушников стали прямоугольниками во всю
  высоту основной кнопки (32 px) и шириной около трети кнопки: по ним легко
  попасть, группы не наслаиваются и не выходят за панель (сайдбар расширен до
  256 px, у групп фиксированная ширина 36 px);
- поповеры и меню теперь позиционируются от рабочей области: новая
  `useAnchoredPosition` прижимает панель к краям окна, выбирает сторону с
  большим запасом места и ограничивает высоту, а содержимое прокручивается —
  подписи устройств и слайдер громкости больше не обрезаются;
- тесты: геометрия группы стрелки, открытие меню и ограничение положения
  панели в пределах окна (289 тестов Vitest).
2026-09-20 14:35:08 +03:00
grendervill a126f50468 feat(web): виды сообщений, автокомплиты композера и ссылки на комнаты
- лента оформляет виды сообщений от сервера: action (/me) — имя акцентом и
  курсив, whisper — приглушённый блок с бейджем, scream — крупнее и жирнее,
  private (/ls) — рамка, замок и бейдж «лично», system — центрированная строка
  без аватара и панели действий; реакции, ответы, правка и удаление работают
  как раньше, группировка не сливает нестандартные виды с обычными;
- автокомплиты в поле ввода с навигацией ↑/↓, Enter/Tab, Escape и мышью:
  @ — участники сервера (в DM собеседник), # — видимые текстовые комнаты,
  : — словарь на 113 эмодзи, / — команды с описанием; подсказки не появляются
  внутри слова и в блоках кода, вставка восстанавливает каретку и не расходится
  со слоем подсветки;
- `<#id>` рендерится как #имя комнаты из снапшота (иначе #id);
- 287 тестов Vitest (было 262), check/lint/test/build зелёные.
2026-09-20 12:06:22 +03:00
grendervill 65d0bafe0e feat(messages): команды чата, личные сообщения и лимиты
AGENT.md 7.6:

- команды разбираются на сервере: /me (блок действия), /whisper и /wisper
  (шёпот), /scream (громкий блок), «//» экранирует слэш, неизвестная команда
  остаётся обычным текстом, пустой текст у команды — 422;
- /ls <логин> <текст> — личное сообщение: тип private, адресат в mentions,
  получатель проверяется на участие в сервере и доступ к комнате; сообщение не
  отдаётся посторонним ни в истории, ни в поиске (и FTS, и подстрочном) и
  доставляется в Gateway только автору и адресатам;
- лимиты: 10 правок в минуту, 20 уникальных реакций на сообщение, 50
  закреплений на комнату, не более 10 упоминаний, @everyone/@here только с
  MENTION_EVERYONE;
- тесты: команды и экранирование, изоляция личного сообщения (история, поиск,
  третий участник), лимит правок и @everyone.
2026-09-20 11:32:50 +03:00
grendervill 4c8f888b3e feat(web): профиль с аватаром, панель в углу, настройки сервера, скрытые ID
Правки пользователя:

- в «Профиль → Основной» сверху квадратный аватар слева, справа от него
  настройки имени, «О себе», состояния и эмодзи во всю высоту; смена аватара —
  клик по нему, форматы (JPEG, PNG, WebP, GIF до 8 МБ) — в подсказке;
- убраны дубли: часовой пояс только в «Язык и регион», статус — только в
  мини-профиле панели;
- панель пользователя переехала в нижний левый угол под сайдбар, подсказка о
  выходе из сервера — на её прежнее место; панель ужата под 240 px;
- шестерёнка сервера открывает новый пункт настроек «Сервер» (список серверов,
  общее, участники, роли, опасная зона), а «Профиль → Сервер» оставлен только
  для ника и «Применить ко всем серверам»;
- окно настроек получило статический размер 70% рабочей области со скроллом;
- ID пользователей скрыты везде, кроме раздела «Инстанс» у администратора;
- список друзей свёрнут под кнопкой «Друзья (N)» и раскрывается снизу вверх;
- 249 тестов Vitest (было 239), check/lint/test/build зелёные.
2026-09-20 11:24:09 +03:00
grendervill bd5f09336b fix(store): закрытие набора строк в бэкфилле через defer 2026-09-20 11:02:09 +03:00
grendervill 0c1b1a261e fix(store): проверка rows.Err и закрытия в бэкфилле поиска 2026-09-20 11:01:48 +03:00
grendervill 1aef57dfd9 feat(search): нечёткий поиск по сообщениям — по началу слова и по подстроке
Поиск требовал ввести слово целиком. Теперь:

- FTS5 ищет по началу слова: «пр» находит «Привет» (последнее слово запроса
  получает префикс), регистр не важен;
- добавлена нормализованная копия текста (`messages.content_lower`, миграция
  00007): подстрочные совпадения ищутся через LIKE без учёта регистра, включая
  кириллицу (SQLite lower() её не приводит — регистр опускает приложение);
- при старте индекс дополняется у старых сообщений (порциями по 500);
- результаты FTS и LIKE объединяются без дублей, сортировка по новизне;
- тест: «пр» и «ивет» находят все три сообщения с «Привет/приветствие/ПРИВЕТ»,
  посторонний текст не попадает.
2026-09-20 11:01:32 +03:00
grendervill bcfc275bb3 feat(web): друзья, личные беседы и вход только по приглашению
Раздел «Друзья» по запросу пользователя:

- первая иконка рейки — «Друзья», серверы ниже за разделителем; личные беседы
  не засоряют список серверов;
- сайдбар друзей: беседы с точками статуса и бейджами непрочитанного,
  список друзей с кликом на беседу;
- контент: пустое состояние «Добавь друзей для общения» с поиском, поиск по
  логину (debounce), добавление в друзья, входящие и исходящие заявки,
  меню «…» (удалить/заблокировать), поповер последнего входа в часовом поясе
  пользователя (по умолчанию МСК), блок «Присоединиться по приглашению»
  с предпросмотром сервера;
- личный чат на существующих компонентах: права по can_send канала, заголовок
  с именем и статусом собеседника, поиск, пины, ack и typing работают;
- realtime: READY dm_channels, события DM_CHANNEL_CREATE, RELATIONSHIP_UPDATE,
  PRESENCE_UPDATE и MESSAGE_CREATE в беседах;
- новый пользователь: «У вас пока нет серверов», создание сервера и вход по
  приглашению (автовступления нет);
- 239 тестов Vitest (было 219), тест паритета ключей i18n (527 ключей).
2026-09-20 01:52:27 +03:00
grendervill d02ef7527a fix(files): аватары и баннеры доступны авторизованным пользователям
Аватар нельзя было показать в списке друзей: файл без комнаты был доступен
только загрузившему. Добавлено назначение файла (`files.purpose`: attachment |
avatar | banner, миграция 00006): аватары и баннеры отдаются любому
авторизованному пользователю, вложения по-прежнему проверяются по правам
комнаты, а непривязанные загрузки остаются приватными.
2026-09-20 01:39:04 +03:00
grendervill 20eed227ca feat(web): вход с 2FA сразу, панель пользователя, модальные настройки, markdown
По правкам пользователя:

- вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные
  резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход;
- нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором
  статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором
  устройства и громкости, переход в «Аудио-видео») и шестерёнка;
- кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»);
- настройки открываются модальным окном со списком пунктов; «Профиль» имеет
  вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем
  серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по
  умолчанию);
- markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал;
- двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и
  при выделенном тексте);
- 219 тестов Vitest (было 176), check/lint/test/build зелёные.
2026-09-20 01:32:11 +03:00
grendervill f26728113d feat(instance): сервер установщика больше не «главный»
Понятие главного сервера убрано из поведения: сервер, создаваемый установщиком,
обычный (is_main = false), автовступления в него нет, а в настройках инстанса
он остаётся лишь справочно (main_guild_id → AdminGuildID в ответе API).
Клиент от него больше не зависит: новичок начинает с пустым списком серверов.
2026-09-20 01:12:01 +03:00
grendervill 33dc1fec96 feat(social): друзья, личные беседы, присутствие, аватары и часовой пояс
По запросу пользователя (вне очереди AGENT.md §13):

- главного сервера как точки входа больше нет: новичок начинает с пустым
  списком серверов, вход — только по приглашению или созданием своего;
- друзья: поиск по логину с экранированием LIKE, заявки (POST
  /users/@me/relationships), принятие, удаление/отклонение, списки friends/
  incoming/outgoing/blocked;
- личные беседы: POST /users/@me/channels (идемпотентно), GET
  /users/@me/channels со собеседником, статусом и последним сообщением;
  сообщения в DM работают через общие ручки комнат, доступ — только участникам
  (посторонний получает 404, события в Gateway тоже фильтруются);
- присутствие: last_seen_at обновляется при активности, «невидимка» и простой
  дольше двух минут выглядят как офлайн, смена статуса рассылает
  PRESENCE_UPDATE друзьям;
- READY отдаёт dm_channels (собеседник, аватар, статус, последнее сообщение);
- профиль: timezone (по умолчанию Europe/Moscow) в PATCH /users/@me,
  загрузка аватара POST /users/@me/avatar (проверка, что это изображение, в том
  числе по содержимому) и удаление DELETE /users/@me/avatar; старый файл
  удаляется с диска;
- тесты: заявки в друзья, личные беседы и их изоляция, «невидимка», часовой
  пояс и аватар, PRESENCE_UPDATE другу, отсутствие событий DM у постороннего.
2026-09-20 01:11:04 +03:00
grendervill b01eb1f34c feat(web): чат Фазы 2 — лента, композер, реакции, ответы, поиск и вложения
Полноценный текстовый чат в веб-клиенте:

- стор сообщений по комнатам (кэш 200, оптимистичная отправка с nonce,
  применение всех событий Gateway, пагинация курсором, непрочитанные и
  упоминания, сброс при логауте и INVALID_SESSION);
- лента: группировка подряд идущих сообщений одного автора, разделители дат,
  разделитель «новые сообщения», подгрузка истории вверх, пустые состояния;
- разметка: жирный/курсив/код/блок кода/зачёркнутый/цитата/ссылки (только
  http(s)) и упоминания <@id> — безопасный рендер React-узлами без
  dangerouslySetInnerHTML;
- реакции чипами с подсветкой своих, быстрые эмодзи и пикер, панель действий и
  контекстное меню (ответ, реакция, закрепление, правка, удаление, копирование);
- ответы с переходом к оригиналу, inline-правка, «печатает…» с POST /typing раз
  в 3 с, пины в модалке, поиск по комнате с подсветкой и переходом;
- композер: автовысота, Enter/Shift+Enter, лимит 4000, drag&drop и скрепка
  (POST /channels/{id}/files → attachment_ids), обработка 429 с таймером и
  perm.denied с причиной, read-only без SEND_MESSAGES;
- бейджи непрочитанных и упоминаний в сайдбаре, авто-POST /ack при активном окне;
- 176 тестов Vitest (было 100), check/lint/test/build зелёные.
2026-09-20 00:15:18 +03:00
grendervill b22db8f048 fix(tests): загрузки тестов не попадают в рабочее дерево репозитория
Тестовый сервер писал файлы вложения в `internal/server/files`, потому что
DATA_DIR не был задан: теперь тесты используют временный каталог, а
`saveUpload` отказывается работать без настроенного каталога данных вместо
записи в текущий рабочий каталог процесса. Лишние файлы удалены.
2026-09-19 23:49:24 +03:00
grendervill 53b7b77eed feat(web): анимации, меню пользователя и раздельные настройки аккаунта и серверов
- анимации на чистом CSS: появление страниц (gl-page), модалки с обратным
  закрытием, поповеры, каскад списков (gl-stagger), индикаторы активной комнаты
  и сервера, скелетоны с shimmer; prefers-reduced-motion отключает всё;
- нижне-левое меню пользователя: аватар и шестерёнка открывают меню вверх с
  пунктами «Настройки аккаунта», «Настройки серверов», сменой статуса и
  выходом (role=menu, стрелки, Escape, возврат фокуса);
- настройки разделены на два соседних раздела с общим внешним видом:
  /settings/account/* (профиль, безопасность, внешний вид, инстанс) и
  /settings/servers/:guildId (общее, свой ник, участники, роли, опасная зона)
  с панелью выбора сервера — иконка и имя, выбор сохраняется в URL;
- старые пути /settings/{profile,security,appearance,instance} работают
  редиректами; шестерёнка настроек сервера — в шапке сервера рядом с «+»
  и только для владельца/ADMINISTRATOR/MANAGE_GUILD;
- 100 тестов Vitest (было 66), check/lint/test/build зелёные.
2026-09-19 23:42:56 +03:00
grendervill 8619f37cd5 feat(messages): счётчики упоминаний, квота приглашений и read state автора
- упоминания в сообщении увеличивают счётчик упоминаний комнаты у упомянутых
  (store.BumpMentionCount) и отправляют им READ_STATE_UPDATE на все устройства;
- отправка сообщения отмечает его прочитанным для автора;
- подтверждение прочтения сбрасывает упоминания;
- суточная квота 10 приглашений на пользователя (AGENT.md 8.6), администратор
  инстанса её обходит; httpx.NewRateLimiterWindow для произвольных окон;
- тест на рост и сброс счётчика упоминаний.
2026-09-19 23:37:02 +03:00
grendervill 0e39b8e027 feat(files): загрузка и выдача вложений
- POST /api/v1/channels/{id}/files (multipart, ATTACH_FILES): файл пишется на
  диск под идентификатором, в БД хранятся метаданные и sha256; имя файла
  очищается от путей и управляющих символов, объём ограничен MAX_UPLOAD_SIZE;
- GET/HEAD /files/{id}: содержимое с проверкой прав на комнату сообщения
  (невидимая комната и чужие вложения → 404), ETag, immutable-кэш,
  Content-Disposition с RFC 5987 для не-ASCII имён;
- вложения привязываются к сообщению при отправке (attachment_ids), сироты
  ищутся через store.ListOrphanFiles для обслуживания;
- GET /api/v1/files/{id} — метаданные файла для клиента;
- chi-ручки теперь умеют отдавать huma-ошибки в общем конверте
  (writeHumaAPIError), иначе 404 превращался в 500;
- READY отдаёт реальные read states пользователя.

Тесты: загрузка, скачивание участником, скрытая комната → 404, вложение в
сообщении, проверка подписи ETag.
2026-09-19 23:36:16 +03:00
grendervill 63bf1e63b5 fix(invites): код приглашения не подменяется слабым при отказе crypto/rand
Вместо предсказуемого запасного кода ручка возвращает 500: приглашение —
это доступ к серверу, маскировать отказ источника энтропии нельзя.
2026-09-19 23:33:59 +03:00
grendervill 61a84bfeae feat(invites): приглашения, каталог публичных серверов и публичность сервера
- POST /guilds/{id}/invites (CREATE_INVITES): код из 10 символов, лимит
  использований и срок жизни; GET /guilds/{id}/invites (MANAGE_GUILD);
  DELETE /invites/{code} — создатель или MANAGE_GUILD;
- GET /invites/{code} — предпросмотр с карточкой сервера, 410 для истёкших и
  исчерпанных; POST /invites/{code} — принятие с проверкой лимита участников,
  повторное принятие не расходует использование;
- PATCH /guilds/{id} принимает `public`: сервер попадает в каталог и открыт для
  входа без приглашения (главный сервер публиковать нельзя);
- GET /guilds/public — каталог (имя, описание, иконка, число участников);
- аудит invite.create/invite.revoke/invite.accept, события INVITE_CREATE и
  INVITE_DELETE владельцу сессии;
- тесты: полный жизненный цикл приглашения, исчерпание, отзыв, права,
  публичный каталог и вход в публичный сервер.
2026-09-19 23:33:34 +03:00
grendervill e4ffbb05a5 feat(messages): сообщения, реакции, пины, typing, read states и поиск
Бэкенд текстовой связи (AGENT.md 7.6, 7.15, 7.16, 8.6):

- POST/GET/PATCH/DELETE /channels/{id}/messages, ответы и упоминания,
  окно правки 24 часа, права автора или MANAGE_MESSAGES;
- реакции (PUT/DELETE .../reactions/{emoji}) с агрегацией и отметкой «моя»;
- закрепления (GET/PUT/DELETE .../pins) под MANAGE_MESSAGES;
- typing с лимитом 1/3 с и адресной рассылкой всем, кроме автора;
- read states: POST /channels/{id}/ack, синхронизация READ_STATE_UPDATE между
  устройствами пользователя;
- поиск FTS5 по комнате (10/мин) с экранированием запроса;
- лимиты отправки 5/5 с (burst 10) и slowmode комнаты; администратор инстанса
  обходит и то, и другое (AGENT.md 7.19);
- Gateway: DispatchToChannel/DispatchToChannelExcept доставляют события комнат
  только тем, кто видит комнату (VIEW_CHANNEL), права считает общий движок;
- store: messages, message_reactions, channel_read_states, files (загрузка
  файлов появится вместе с вложениями).

Тесты: жизненный цикл сообщения, видимость скрытой комнаты (404 участнику,
200 админу), slowmode, упоминания и ответы, typing и read state, фильтрация
событий комнаты между двумя WS-клиентами.
2026-09-19 23:32:11 +03:00
grendervill 4550bea6cc feat(db): схема Фазы 2 — сообщения, реакции, read states, файлы, инвайты, FTS5
Миграция 00004: таблицы `messages`, `message_reactions`,
`channel_read_states`, `files`, `invites` и виртуальная таблица `messages_fts`
с триггерами синхронизации (AGENT.md 6.1, 7.6, 7.7, 7.9, 7.15, 7.16).

Полнотекстовый поиск требует сборки с тегом `sqlite_fts5`:
- `GO_TAGS ?= sqlite_fts5` в Makefile (build/test/cover) и в Dockerfile;
- build-tags в golangci-lint;
- страж `internal/database/fts5_guard.go`: сборка без тега падает с понятным
  текстом, а не ломается на миграции в рантайме.
2026-09-19 23:29:39 +03:00
grendervill 0a82dd3a02 feat(api,install): исключения fail2ban и смена своего никнейма
- установщик: `[DEFAULT] ignoreip` в jail.local — локальные сети и WireGuard
  исключены всегда, свой адрес добавляется флагом `--ignore-ip <ip|cidr>`;
  админ, часто заходящий в панель, больше не банит сам себя;
- участник может менять свой никнейм на сервере без MANAGE_NICKNAMES и без
  проверки иерархии (тайм-аут на себя по-прежнему запрещён);
- тест на смену своего ника и запрет self-тайм-аута.
2026-09-19 23:27:26 +03:00
grendervill ac2b2cb18a fix(install): ключ шифрования бэкапов создаётся после установки age
На чистой машине age-keygen появляется только на шаге install_packages,
поэтому генерация ключа в generate_secrets падала, а chmod на несуществующий
файл валил установку. Теперь ensure_age_key вызывается после установки
пакетов, безопасно пропускает chmod и обновляет AGE_RECIPIENT в .env.
Регрессия найдена прогоном scripts/test-install.sh в контейнере.
2026-09-19 22:11:31 +03:00
grendervill fb221c91bb fix(install): повторный запуск установщика больше не меняет секреты
Найдено на стенде: повторный `install.sh` без `--reconfigure` генерировал новые
MASTER_KEY/SESSION_PEPPER/TOTP_ENCRYPTION_KEY, после чего email-индекс и
TOTP-секреты существующих пользователей перестали расшифровываться (вход
возвращал `auth.invalid_credentials`).

- секреты переиспользуются при любом повторном запуске, а не только с
  `--reconfigure`;
- добавлен явный флаг `--rotate-secrets` с предупреждением о последствиях;
- age-ключ бэкапов сохраняется, старые бэкапы остаются читаемыми;
- защита от потери `.env`: если база есть, а MASTER_KEY нет — установщик
  останавливается с объяснением вместо молчаливой генерации новых ключей;
- `scripts/test-install.sh` сравнивает секреты до и после повторной установки
  и падает при их изменении.
2026-09-19 22:08:47 +03:00
grendervill 9c001fedfe feat(web): базовый клиент Фазы 1 — вход, серверы, комнаты, настройки
Клиент React 19 + TanStack Query + Zustand:

- вход (поле TOTP появляется на `auth.2fa_required`), регистрация с проверками
  и учётом `registration_enabled`, онбординг первого входа с возможностью
  пропустить, редирект `/` и страница `/status`;
- защита маршрутов: неавторизованных — на `/login`, без онбординга — на
  `/onboarding`;
- оболочка `/app`: рейка серверов, сайдбар категорий и комнат, шапка сервера,
  панель пользователя, модалки создания сервера и комнаты, экран «нет серверов»
  со вступлением в главный сервер, заглушка комнаты до Фазы 2;
- настройки: профиль, безопасность (step-up, сессии, logout-all, 2FA с
  локальным QR и кодами восстановления), внешний вид, админ-раздел инстанса;
- Gateway-клиент: HELLO/IDENTIFY/RESUME, heartbeat с ожиданием ACK,
  экспоненциальное переподключение, разбор `INVALID_SESSION {reason,resumable}`;
- диспетчер событий: READY/RESUMED/USER_UPDATE/GUILD_UPDATE/GUILD_DELETE/
  CHANNEL_* применяются к стору, GUILD_CREATE догружается по REST,
  MEMBER_*/ROLE_* инвалидируют запросы участников, ролей и карточки сервера;
- i18n ru/en, 66 тестов Vitest, `check`/`lint`/`test`/`build` зелёные.

Серверные правки под клиент:

- `totp_enabled` в профиле (`GET /users/@me`, вход, регистрация) — клиенту
  нужно знать, требовать ли код при step-up;
- `POST /auth/2fa/setup` отдаёт `otpauth_url` и `qr_png` (data-URI): QR-код
  рисуется локально, внешние сервисы генерации QR не используются;
- права в ответах — имена (`VIEW_CHANNEL|SEND_MESSAGES`), клиент сверяет имена.
2026-09-19 22:03:46 +03:00
grendervill d6ca8e420f fix(gateway): имена событий и resumable-флаг по спецификации §8.3
- ROLE_CREATE/ROLE_UPDATE/ROLE_DELETE вместо GUILD_ROLE_*;
- выдача/снятие роли приходит как MEMBER_UPDATE с полным списком role_ids:
  клиент пересчитывает доступные действия сразу после события;
- промах RESUME отдаёт INVALID_SESSION с {"resumable": true} и следом полный
  READY — сессия сохраняется, повторный вход не нужен;
- тесты обновлены под новый контракт.
2026-09-19 21:57:46 +03:00
grendervill 463021161a test(smoke): проверки API Фазы 1 в smoke-тесте
Добавлен блок [2b]: `/api/v1/instance` с созданным главным сервером,
наличие ручек серверов в OpenAPI, 401 на `/users/@me` без сессии, отказ
регистрации на пустое тело и отклонение обычного GET на `/gateway`
(апгрейд обязателен).
2026-09-19 21:56:47 +03:00
grendervill b189b3592b feat(api): список событий безопасности аккаунта
GET /users/@me/security-events отдаёт последние входы, неудачные попытки,
смены пароля и изменения 2FA (AGENT.md 7.1: уведомления о новых входах —
основа для настроек безопасности в клиенте).
2026-09-19 21:55:52 +03:00
grendervill bf0d130ee8 feat(cli): включение 2FA инстанс-админа командой totp-setup
2FA обязательна для инстанс-администраторов, но до её включения вход закрыт —
получался замкнутый круг. Добавлены команды обслуживания:

- `glchat totp-setup --email <admin>`: создаёт секрет, подтверждает его кодом,
  печатает секрет, otpauth-ссылку и 8 резервных кодов (каждый одноразовый);
- `glchat totp-reset --email <admin>`: удаляет секрет при потере устройства;
- код `auth.2fa_enrollment_required` с подсказкой, какую команду выполнить;
- установщик: флаг `--admin-2fa` для автоматического включения (по умолчанию
  печатает подсказку, чтобы секреты не оседали в логах установки).

Проверено сквозным прогоном локально: bootstrap → 403 на входе без 2FA →
totp-setup → вход с TOTP-кодом → профиль, серверы, комнаты, роли, участники,
аудит, создание сервера админом, 429 на шестой попытке входа, секретов в логах
нет.
2026-09-19 21:53:35 +03:00
grendervill c059cd9f51 feat(api): лимиты частоты запросов (AGENT.md 8.6)
- httpx.RateLimiter: token bucket в памяти с уборкой неактивных ключей и
  подменяемым источником времени;
- login/register — 5 запросов в минуту на IP, весь API — 120 в минуту на
  пользователя (по хэшу токена сессии) или на IP для анонимных запросов;
- превышение отдаёт 429 с Retry-After и retry_after_ms в конверте ошибки;
- тесты: наполнение и пополнение ведра, независимость ключей, 429 на ручке
  входа.
2026-09-19 21:51:47 +03:00
grendervill 6d7062dc2f test(api): сценарии §11.5 и §11.6 — админ инстанса и realtime-события
- администратор инстанса вне сервера видит и меняет чужой сервер: комнаты,
  роли, участники, аудит; его действия помечены actor_instance_admin;
- администратора инстанса нельзя исключить или замутить с сервера
  (instance.admin_protected) — права глобальны и не отзываются владельцем;
- realtime-проверка: два WS-клиента (владелец и участник) получают события
  CHANNEL_CREATE, GUILD_UPDATE, GUILD_ROLE_CREATE, MEMBER_ROLES_UPDATE от
  REST-ручек, а USER_UPDATE приходит только владельцу сессии.
2026-09-19 21:50:54 +03:00
grendervill 1b1a679827 feat(api): ручки Фаз(ы) 1 — профиль, серверы, роли, админ инстанса
REST-слой Фазы 1 на huma (OpenAPI 3.1 генерируется из кода):

- профиль: GET/PATCH /users/@me, смена пароля со step-up, публичный профиль,
  завершение онбординга (новая миграция 00003 с onboarding_completed_at);
- серверы: создание/изменение/удаление, join/leave, список серверов
  пользователя, журнал действий;
- комнаты: список с учётом прав, создание/изменение/удаление;
- участники: список с профилями и ролями, никнейм, тайм-аут, исключение;
- роли: CRUD, выдача/снятие с проверкой иерархии и запретом выдачи прав выше
  собственных;
- админ инстанса: публичная информация, настройки, серверы, пользователи,
  аудит, выдача прав администратора со step-up; обход лимитов фиксируется в
  аудите отдельной записью limits.bypass;
- движок прав: участие в сервере стало обязательным условием (IsMember),
  не участник не получает прав роли @user; калькулятор прав общий для API и
  Gateway, инвалидация кэша после изменений;
- Gateway: браузерный клиент аутентифицируется cookie на рукопожатии, IDENTIFY
  без токена использует её; события GUILD/CHANNEL/MEMBER/ROLE рассылаются из
  ручек, USER_UPDATE — адресно;
- ошибки huma отдаются в едином конверте {"error":{"code","message"}}.

Тесты: 8 сценариев API (профиль, жизненный цикл сервера и права, лимиты и
обход админом, иерархия ролей, тайм-аут, скрытие комнаты оверрайдом,
членство в движке прав, cookie-идентификация Gateway).
2026-09-19 21:50:06 +03:00
grendervill 86dff94a02 feat(gateway): WebSocket Gateway с HELLO, IDENTIFY и READY
Реализован Gateway по AGENT.md §8.3:

- оп-коды 0/1/2/3/4/7/10/11, единый конверт {op,t,d,s};
- HELLO с интервалом сердцебиения (45 с) и идентификатором сессии;
- IDENTIFY/RESUME с проверкой токена через auth.ResolveSession;
- READY: пользователь, серверы, видимые комнаты, роли, участники и права,
  собранные через движок прав (VIEW_CHANNEL-фильтрация);
- HEARTBEAT → HEARTBEAT_ACK, лимит частоты IDENTIFY (5 с);
- RESUME: буфер на 1000 событий с TTL 5 минут, догон пропущенных событий и
  подтверждение RESUMED, полный READY при расхождении состояния;
- рассылка Dispatch/SendToUser с буферизацией для отключённых сессий;
- проверка Origin по allowlist доменов инстанса (AGENT.md §9.7);
- httpx.statusRecorder пробрасывает Hijack, иначе апгрейд ломается.

Тесты: 6 сценариев Gateway (READY, скрытые комнаты, неверный токен,
heartbeat, dispatch, RESUME) и 2 теста маршрута /gateway через полный стек
middleware, включая отклонение чужого Origin.
2026-09-19 21:42:41 +03:00
grendervill c0537d3c17 feat(bootstrap): первичная настройка инстанса и CLI-команды
- internal/bootstrap: создание инстанс-админа, главного сервера, ролей
  «Администратор» и «Пользователь» (all permissions / дефолтный набор §6.3),
  общей текстовой комнаты; повторный запуск идемпотентен
- админ получает флаг is_instance_admin и системный бейдж instance_admin
- CLI: glchat bootstrap-admin, reset-password, make-admin, remove-admin
  (работают внутри контейнера через DATA_DIR)
- auth: UserByEmail, SetPassword (отзывает сессии), RegisterAdmin (в обход
  флага регистрации — bootstrap выполняет установщик)
- тесты: создание админа и главного сервера, идемпотентность, автовступление
  нового пользователя с ролью @user
2026-09-19 21:37:13 +03:00
grendervill f61751ed26 feat(api): REST на chi + huma с auth-ручками и OpenAPI 3.1
- переход на chi + huma (решение D-006): huma отдаёт типизированные ручки и
  генерирует документ, auth-ручки живут на chi (cookie и заголовки напрямую)
- единый формат ошибок: код (auth.invalid_credentials, auth.2fa_required,
  perm.denied и т.д.) + человекочитаемое сообщение (AGENT.md 8.5)
- cookie сессии __Host-session: HttpOnly, SameSite=Lax, Secure при TLS;
  альтернатива — Bearer-токен для desktop/CLI (AGENT.md 8.1)
- ручки: register, login, logout, logout-all, sessions, step-up, 2fa/setup,
  2fa/enable, users/@me; IP и User-Agent прокидываются из запроса в контекст
- /api/v1/openapi.json: объединённый документ (схемы huma + контракт auth)
- тесты: регистрация через API с cookie, ошибки входа, обязательная сессия,
  валидация, наличие всех путей в OpenAPI
2026-09-19 21:36:00 +03:00
grendervill 16218ee045 feat(auth): сервис аккаунтов и аутентификации Фазы 1
- регистрация: политика пароля (≥10 + локальный словарь 10k утечек, SecLists MIT),
  username и email без учёта регистра, шифрование email с привязкой к аккаунту,
  blind index для поиска, автовступление в главный сервер с ролью @user
- вход: Argon2id+pepper, TOTP при включённом 2FA, блокировка после серии неудач,
  события безопасности; единый ответ на неверный пароль и неизвестный аккаунт
- сессии: opaque-токены (в БД только SHA-256), ротация токена, logout/logout-all,
  отзыв остальных сессий при смене пароля, step-up с окном 10 минут
- 2FA: настройка секрета с QR-URL, подтверждение кодом, 8 резервных кодов
  (хранятся хэшами), обязательность для инстанс-админов, запрет отключения
- тесты на реальной SQLite: регистрация/вход/дубликаты, слабые и утёкшие пароли,
  ротация и logout-all, TOTP и резервные коды, step-up, смена пароля,
  выключенная регистрация, отсутствие секретов в логах
2026-09-19 21:29:07 +03:00
grendervill a5205cc054 feat(store,permissions): схема Фазы 1, доступ к данным и движок прав
- миграция 00002: users, sessions, totp_secrets, webauthn_credentials,
  security_events, guilds, guild_members, roles, member_roles, channels,
  channel_overrides, audit_log + дефолтные instance_settings (AGENT.md 6.1)
- internal/store: Snowflake-идентификаторы, CRUD пользователей и сессий
  (ротация, step-up, logout-all), TOTP и события безопасности, серверы,
  участники, роли, комнаты и оверрайды, настройки инстанса и аудит
- internal/permissions: 37 прав битмаской, вычисление по правилам §6.2
  (баз role @user → оверрайды ролей → оверрайд пользователя → ADMINISTRATOR),
  иерархия ролей и участников, тайм-ауты, обход для инстанс-админа, LRU-кэш
  с инвалидацией
- internal/source: адаптер permissions.Source поверх store
- тесты: 18 unit-тестов прав + интеграционный набор на реальной SQLite
  (приватная комната, модератор, владелец, инстанс-админ, тайм-аут)
- golangci: обоснованное исключение gosec для пакета store (конверсии
  Snowflake и сборка SQL из константных шаблонов)
2026-09-19 21:24:17 +03:00
grendervill 06e313dcc6 feat(crypto): крипто-слой Фазы 1 — Argon2id+pepper, AES-256-GCM, blind index
- internal/crypto/password.go: Argon2id с параметрами под 1 vCPU (OWASP),
  обязательный pepper (HMAC-SHA-256 до Argon2id), разбор формата хэша,
  NeedsRehash для калибровки параметров
- internal/crypto/secretbox.go: AES-256-GCM с версионированным ключом (v1:),
  разделение мастер-ключа на подключи шифрования и blind index (HMAC-SHA-256),
  blind index для поиска по email без расшифровки (§9.2)
- internal/crypto/tokens.go: opaque-токены сессий (хранится только SHA-256),
  резервные коды 2FA, нормализация email, политика пароля и username
- тесты: соль, pepper, версии ключа, чужие шифротексты, стабильность blind index
- go.mod: добавлены chi v5, huma v2, pquerna/otp (подключаются следующими шагами)
2026-09-19 21:17:52 +03:00
grendervill a671027461 test(smoke): проверка обновления сертификата TURN и работоспособности LiveKit после неё 2026-09-19 21:14:39 +03:00
grendervill ae289fc756 fix(turn): встроенный TURN анонсируется по публичному адресу релея
Если задан --turn-relay-host, turn.domain указывает на релей, а не на
turn.<domain>: иначе клиенты получали недостижимый TURN-кандидат
(порт 3478 на VPS обслуживает coturn, а не домашний LiveKit).
Также добавлена smoke-проверка обновления сертификата TURN.
2026-09-19 21:12:11 +03:00
grendervill d54a8fcc2d fix(smoke): проверка TURN/TLS по результату верификации, а не по разбору CN 2026-09-19 20:54:49 +03:00
grendervill 2d7d8406d2 fix(security): DOCKER-USER пропускает доверенную подсеть (прокси по WireGuard)
Правила блокировали 8080/7880 для всех, кроме docker-сетей, поэтому внешний
прокси не мог дойти до приложения. Теперь разрешена подсеть --allow-from,
правила снимаются перед повторным применением (не дублируются).
2026-09-19 20:51:50 +03:00
grendervill cb2e223a4d feat(ufw): --allow-port/--allow-from для доступа прокси к внутреннему порту 2026-09-19 20:48:59 +03:00
grendervill dcb2a8b3fa fix(smoke): проверки используют адрес привязки, а не localhost 2026-09-19 20:46:43 +03:00
grendervill 891b34be13 fix(turn): диапазон релея ограничен 20 портами (docker не тянет 10k DNAT-правил) 2026-09-19 20:44:25 +03:00
grendervill f1a5382413 fix(compose): публикация портов TURN (3478/udp, 5349/tcp, диапазон релея) 2026-09-19 20:41:37 +03:00
grendervill 2513dde5d6 fix(deploy): в файл значений .env добавлены TURN_RELAY_USER/PASSWORD 2026-09-19 20:39:08 +03:00