Commit Graph

11 Commits

Author SHA1 Message Date
grendervill cebce0ae3b feat(auth): вход через OAuth-провайдеры (GitHub, Google, Discord)
Фаза 7, AGENT.md 7.1: провайдеры включаются переменными окружения
OAUTH_<PROVIDER>_CLIENT_ID/SECRET, привязка внешнего аккаунта идёт по
подтверждённому провайдером email через blind index, вход забаненному на
инстансе запрещён, все входы и привязки попадают в события безопасности,
привязка — ещё и в аудит инстанса.

Внешний идентификатор (subject) хранится только индексом HMAC-SHA-256,
токены провайдеров не сохраняются вовсе; state подписывается ключом сессий
и ограничен по времени, редирект после входа — только внутренний путь.
Если провайдер не настроен, ручка отвечает oauth.provider_not_configured,
а клиент показывает человеческий текст вместо кнопки.

Клиент: кнопки входа по списку включённых провайдеров на экране входа и
раздел «Вход через внешние сервисы» в настройках безопасности; адрес
возврата для настроек приложения виден в GET /auth/oauth/providers.

Миграция 00019 добавляет таблицу oauth_accounts. Установщик и .env.example
знают про OAUTH_* и сохраняют значения при --reconfigure.

Тесты: Go — выключенный провайдер, подписанный state (подмена и чужой
провайдер), создание и повторный вход, привязка к существующему аккаунту,
неподтверждённый email, выключенная регистрация, бан инстанса, mocked
провайдер (httptest) для обмена кода; web — кнопки провайдеров и ошибка
возврата; серверная ручка отдаёт понятный отказ без настроек.
2026-09-26 15:13:24 +03:00
grendervill cd1d662572 feat(auth): вход по ключу доступа (passkeys, WebAuthn)
Фаза 7, AGENT.md 7.1: регистрация ключа в настройках безопасности с
обязательным step-up, вход по ключу без пароля (в т.ч. без ввода почты —
обнаруживаемый ключ), несколько ключей на аккаунт, отзыв и переименование,
события безопасности и аудит.

Сервер: github.com/go-webauthn/webauthn (BSD-3-Clause), RP ID и Origin
берутся из конфига домена; если домен — IP-адрес (стенд без домена),
passkeys честно выключены (auth.passkey_unsupported). Церемонии живут в
памяти процесса 5 минут и одноразовые: повторная отправка challenge
отклоняется. Миграция 00018 пересобирает неиспользуемую таблицу
webauthn_credentials под полную запись credential в JSON. Новые ручки
входа ограничены по IP (10/мин).

Клиент: тонкая обёртка над navigator.credentials без тяжёлых SDK,
раздел «Ключи доступа» в настройках безопасности и кнопка «Войти по ключу»
на экране входа; понятные сообщения для браузеров без поддержки WebAuthn
и при отмене диалога.

Тесты: Go — регистрация/вход с эмулятором аутентификатора (реальная
проверка подписи P-256), отказ при чужом challenge, одноразовость
церемонии, обязательный step-up при управлении ключами, запрет входа
забаненному, ограничение allowCredentials при входе с почтой, лимит и
валидация имени; web — 12 vitest с моком navigator.credentials;
Playwright — живой сценарий с виртуальным аутентификатором Chromium.
2026-09-26 15:12:43 +03:00
grendervill 498201b413 feat(server): инстанс отдаёт манифест автообновления desktop-обёртки
Обёртка с первого инкремента Фазы 6 запрашивает
`GET /updates/{target}/{arch}/{current_version}`, но на инстансе этого пути не
было: запрос попадал в SPA-заглушку, клиент получал HTML с кодом 200 и писал
в лог «error decoding response body».

Что сделано:

- `internal/server/updates.go` — статика автообновления: каталог `UPDATES_DIR`
  (в контейнере `/app/updates:ro`), раскладка `<target>/<arch>/<version>.json` +
  артефакт и `.sig`; версия без расширения отображается на `<version>.json`,
  а если его нет — на `latest.json` (обёртка приходит со своей установленной
  версией, «новее ли релиз» решает клиент сравнением semver). Для имён
  с расширением артефакта запасного варианта нет: иначе вместо архива клиент
  скачал бы манифест и подпись не сошлась бы. Сегменты пути проверяются по
  символам, выход за каталог невозможен; `/updates/` добавлен в reservedPrefixes,
  поэтому отсутствующий манифест — честный 404, а не HTML-шелл;
- `scripts/desktop-release.sh` и цели `make desktop-release` /
  `make desktop-release-upload HOST=…`: берут собранный `glchat.app.tar.gz` + `.sig`,
  определяют target/arch по имени артефакта, копируют их в каталог инстанса и
  собирают манифест (версия из `tauri.conf.json`), объединяя платформы одной
  версии; есть `--dry-run` и загрузка на инстанс по SSH;
- `deploy/install.sh` и шаблоны compose: каталог `${GLCHAT_UPDATES_DIR}`
  (`/opt/glchat/updates`) создаётся установщиком и монтируется в контейнер
  только для чтения; в `.env` добавлен `GLCHAT_UPDATES_DIR`.

Владельцу внешнего прокси добавлять ничего не нужно: `/updates/...` уходит в
catch-all `handle` и доходит до приложения, как статика клиента.

Проверено на стенде: `curl https://gl.mhspx.su/updates/darwin/aarch64/0.1.0`
→ 200 `application/json; charset=utf-8`, артефакт → 200 `application/gzip`,
отсутствующие платформа и версия → 404 (не HTML), `readyz` → 200, обёртка
в логе пишет «обновлений нет» вместо ошибки разбора. Тесты: 6 новых в
`internal/server/updates_test.go`, манифест разбирается типом самого плагина
обновлений (`updates::tests::манифест_разбирается_плагином_обновлений`).
2026-09-22 23:17:22 +03:00
grendervill 5afcfee744 feat(ops): чистка данных, reindex и PWA с офлайн-режимом
- internal/retention: истёкшие сессии, аудит старше RETENTION_AUDIT_DAYS и
  файлы без ссылок старше RETENTION_ORPHAN_HOURS; сиротой файл считается
  только если на него не ссылается ничего (вложения, аватары, оформление
  сервера, комнат, ролей, эмодзи, звуки, вебхуки, приглашения)
- обслуживание запускается в приложении по RETENTION_INTERVAL_SECONDS;
  команды glchat cleanup и glchat reindex делают то же вручную
- PWA: манифест с иконками и shortcuts, офлайн-страница, сервис-воркер
  (кэш только /assets/*, приватные запросы не кэшируются)
- тесты: Go на чистку (сирота удаляется, используемые файлы и свежие сироты
  остаются, истёкшая сессия уходит), Vitest на манифест и регистрацию воркера
2026-09-21 19:42:12 +03:00
grendervill 1552146aa1 feat(voice): модерация через RoomService, саундборд и звуковая палитра
AGENT.md 7.13, 7.14:

- `internal/voice/admin.go` — клиент RoomService (Twirp/JSON, без SDK):
  GetParticipant, ListParticipants, MutePublishedTrack, RemoveParticipant с
  административным токеном; внутренний адрес SFU задаётся LIVEKIT_API_URL;
- серверный мьют теперь применяется и на стороне SFU (клиент не обойдёт),
  кик из голосовой (DELETE /guilds/{id}/voice-states/{user_id}) удаляет
  участника из комнаты через RemoveParticipant, перемещение — тоже;
- саундборд и звуковая палитра: таблица `guild_sounds` (миграция 00010),
  загрузка MP3/OGG/WAV/WebM/M4A до 512 КБ с MANAGE_SOUNDS, отдельные лимиты
  30 + 30, переименование, удаление, список с фильтром по виду;
- проигрывание POST /guilds/{id}/sounds/{sound_id}/play: нужно право
  USE_SOUNDBOARD и присутствие в голосовой комнате, событие SOUNDBOARD_PLAY
  уходит только участникам этой комнаты (звук играют клиенты), лимит 3 звука
  за 10 секунд; набор звуков приходит в READY и обновляется событием
  GUILD_SOUNDS_UPDATE;
- тесты: права на загрузку, требование события для звука интерфейса, запрет
  проигрывания вне комнаты, лимит частоты, переименование и удаление.
2026-09-20 19:01:14 +03:00
grendervill 9e7d023f7e feat(voice): время жизни токена LiveKit настраивается (по умолчанию 10 минут)
AGENT.md 7.14: токен выдаётся на 10 минут и переиздаётся клиентом до истечения.
Значение задаётся LIVEKIT_TOKEN_TTL_SECONDS.
2026-09-20 18:32:47 +03:00
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 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 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 3d1816a640 feat(config): схема URL в meta зависит от TLS_ENABLED
- при установке без TLS (--skip-tls, стенд без домена) клиент получает
  http:// и ws:// вместо неработающих https:// и wss://
- установщик пишет TLS_ENABLED в .env; тесты фиксируют обе схемы
2026-09-19 19:58:32 +03:00
grendervill 69aa98e18c feat(server): скелет Go-сервера с healthz, readyz и api/v1/meta
- internal/config: конфигурация из env с валидацией и дефолтами (§5.1, §9.1)
- internal/httpx: middleware (request id, slog-логи, recover, security-заголовки),
  единый формат ошибок §8.5 и лимит тела запроса
- internal/database: SQLite WAL + busy_timeout + foreign_keys, один writer
  и пул чтения, goose-миграции embedded, PRAGMA optimize по расписанию
- internal/meta: полезная нагрузка /api/v1/meta (версия, фичи, лимиты)
- internal/server: маршруты, SPA-раздача собранного клиента, OpenAPI 3.1
- cmd/glchat: точка входа с graceful shutdown и JSON-логами
- тесты: конфиг, миграции и идемпотентность, HTTP-эндпоинты, статика, 404/405
2026-09-19 18:20:56 +03:00