refactor: убрать вход через внешние провайдеры (OAuth) полностью

Решение владельца 27.09.2026 (D-084): сервис ставится на сервер конкретного
человека, и ему всё равно нужен свой OAuth у провайдера — поддержка общего
входа только создаёт трение (регистрация приложений, redirect URI, модерация,
чужие ключи в конфиге). Способы входа остаются: пароль + 2FA и ключи доступа
(WebAuthn).

Удалено:
- сервер: internal/auth/oauth.go, internal/server/oauth.go, internal/store/oauth.go
  и их тесты; поля и методы конфига OAuth*; oauthLimiter и регистрация ручек;
  отображение ошибок oauth.*; features.oauth_enabled/oauth_providers в /meta;
- клиент: web/src/api/oauth.ts, раздел «Вход через внешние сервисы», кнопки
  провайдеров на странице входа, ключи i18n (ru/en), тесты и фикстуры;
- установщик: переменные OAUTH_* из .env, .env.example и шаблона (хелпер
  чтения существующих значений переименован в existing_value — он остался нужен
  для VAPID_SUBJECT);
- зависимость golang.org/x/oauth2 (go mod tidy).

Схема: миграция 00027 удаляет таблицу oauth_accounts (00019 не переписываем —
она применена на стендах). Откат миграции возвращает структуру; тест
TestOAuthRemovalMigration проверяет накат, откат и повторный накат.

AGENT.md (локальный) помечает пункты про OAuth как отменённые.
This commit is contained in:
2026-09-27 18:27:33 +03:00
parent c88f774749
commit a48b865bc9
27 changed files with 100 additions and 2287 deletions
+5 -52
View File
@@ -718,9 +718,10 @@ generate_secrets() {
# расшифровывает email/PII и TOTP-секреты, SESSION_PEPPER — blind index,
# LIVEKIT_API_SECRET подписывает токены. Их смена делает существующие
# данные нечитаемыми, поэтому она возможна только явным --rotate-secrets.
# oauth_value отдаёт значение OAuth-настройки: сначала из окружения
# (оператор задал при запуске), затем из существующего .env.
oauth_value() {
# existing_value отдаёт значение необязательной настройки: сначала из
# окружения (оператор задал при запуске), затем из существующего .env —
# чтобы `--reconfigure` её не потерял.
existing_value() {
local key="$1"
local value="${!key:-}"
if [ -z "$value" ] && [ "$env_existing" = "1" ]; then
@@ -774,7 +775,7 @@ generate_secrets() {
fi
# Контакт администратора для push-сервисов: значение можно задать заранее,
# иначе берём email администратора или адрес домена.
VAPID_SUBJECT="$(oauth_value VAPID_SUBJECT)"
VAPID_SUBJECT="$(existing_value VAPID_SUBJECT)"
if [ -z "$VAPID_SUBJECT" ]; then
if [ -n "${ADMIN_EMAIL:-}" ]; then
VAPID_SUBJECT="mailto:${ADMIN_EMAIL}"
@@ -783,21 +784,6 @@ generate_secrets() {
fi
fi
# OAuth (Фаза 7) не генерируется: клиентские id/секреты выдаёт провайдер.
# Значение берём из окружения, иначе сохраняем прежнее из .env — иначе
# `--reconfigure` молча выключил бы вход через провайдеров.
OAUTH_VK_CLIENT_ID="$(oauth_value OAUTH_VK_CLIENT_ID)"
OAUTH_VK_CLIENT_SECRET="$(oauth_value OAUTH_VK_CLIENT_SECRET)"
OAUTH_YANDEX_CLIENT_ID="$(oauth_value OAUTH_YANDEX_CLIENT_ID)"
OAUTH_YANDEX_CLIENT_SECRET="$(oauth_value OAUTH_YANDEX_CLIENT_SECRET)"
OAUTH_GITHUB_CLIENT_ID="$(oauth_value OAUTH_GITHUB_CLIENT_ID)"
OAUTH_GITHUB_CLIENT_SECRET="$(oauth_value OAUTH_GITHUB_CLIENT_SECRET)"
OAUTH_GOOGLE_CLIENT_ID="$(oauth_value OAUTH_GOOGLE_CLIENT_ID)"
OAUTH_GOOGLE_CLIENT_SECRET="$(oauth_value OAUTH_GOOGLE_CLIENT_SECRET)"
OAUTH_DISCORD_CLIENT_ID="$(oauth_value OAUTH_DISCORD_CLIENT_ID)"
OAUTH_DISCORD_CLIENT_SECRET="$(oauth_value OAUTH_DISCORD_CLIENT_SECRET)"
OAUTH_REDIRECT_BASE="$(oauth_value OAUTH_REDIRECT_BASE)"
if [ "$DRY_RUN" = "1" ]; then
log_file_only "DRY-RUN: секреты сгенерированы в памяти (в файл не пишутся)"
ok "секреты подготовлены (dry-run)"
@@ -972,24 +958,6 @@ BACKUP_OFFSITE_TARGET=@BACKUP_OFFSITE_TARGET@
BACKUP_OFFSITE_KEEP_DAYS=@BACKUP_OFFSITE_KEEP_DAYS@
AGE_RECIPIENT=@AGE_RECIPIENT@
# --- OAuth-провайдеры (Фаза 7) ---
# Пусто — провайдер выключен. Адрес возврата: <домен>/api/v1/auth/oauth/<id>/callback.
# VK ID: id.vk.ru → «Мои приложения»; секрет — сервисный ключ доступа
# (для конфиденциального приложения он обязателен при обмене кода на токен).
OAUTH_VK_CLIENT_ID=@OAUTH_VK_CLIENT_ID@
OAUTH_VK_CLIENT_SECRET=@OAUTH_VK_CLIENT_SECRET@
# Яндекс ID: oauth.yandex.ru → «Создать приложение» (права login:email, login:info).
OAUTH_YANDEX_CLIENT_ID=@OAUTH_YANDEX_CLIENT_ID@
OAUTH_YANDEX_CLIENT_SECRET=@OAUTH_YANDEX_CLIENT_SECRET@
OAUTH_GITHUB_CLIENT_ID=@OAUTH_GITHUB_CLIENT_ID@
OAUTH_GITHUB_CLIENT_SECRET=@OAUTH_GITHUB_CLIENT_SECRET@
OAUTH_GOOGLE_CLIENT_ID=@OAUTH_GOOGLE_CLIENT_ID@
OAUTH_GOOGLE_CLIENT_SECRET=@OAUTH_GOOGLE_CLIENT_SECRET@
OAUTH_DISCORD_CLIENT_ID=@OAUTH_DISCORD_CLIENT_ID@
OAUTH_DISCORD_CLIENT_SECRET=@OAUTH_DISCORD_CLIENT_SECRET@
# Необязательно: внешний адрес инстанса для redirect_uri (пусто — из домена).
OAUTH_REDIRECT_BASE=@OAUTH_REDIRECT_BASE@
# --- Web Push (Фаза 7) ---
# Приватный VAPID-ключ (PKCS#8 DER в base64). Пусто — Web Push выключен.
VAPID_PRIVATE_KEY=@VAPID_PRIVATE_KEY@
@@ -1066,21 +1034,6 @@ LIVEKIT_API_KEY=${LIVEKIT_API_KEY}
LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}
LIVEKIT_PUBLIC_URL=${LIVEKIT_PUBLIC_URL}
# OAuth-провайдеры (Фаза 7): пусто — провайдер выключен. Кавычки добавляет
# установщик только для значений со спецсимволами (docker compose разбирает
# .env своим парсером и пустые кавычки ему не нравятся).
OAUTH_VK_CLIENT_ID=$(env_quote "$OAUTH_VK_CLIENT_ID")
OAUTH_VK_CLIENT_SECRET=$(env_quote "$OAUTH_VK_CLIENT_SECRET")
OAUTH_YANDEX_CLIENT_ID=$(env_quote "$OAUTH_YANDEX_CLIENT_ID")
OAUTH_YANDEX_CLIENT_SECRET=$(env_quote "$OAUTH_YANDEX_CLIENT_SECRET")
OAUTH_GITHUB_CLIENT_ID=$(env_quote "$OAUTH_GITHUB_CLIENT_ID")
OAUTH_GITHUB_CLIENT_SECRET=$(env_quote "$OAUTH_GITHUB_CLIENT_SECRET")
OAUTH_GOOGLE_CLIENT_ID=$(env_quote "$OAUTH_GOOGLE_CLIENT_ID")
OAUTH_GOOGLE_CLIENT_SECRET=$(env_quote "$OAUTH_GOOGLE_CLIENT_SECRET")
OAUTH_DISCORD_CLIENT_ID=$(env_quote "$OAUTH_DISCORD_CLIENT_ID")
OAUTH_DISCORD_CLIENT_SECRET=$(env_quote "$OAUTH_DISCORD_CLIENT_SECRET")
OAUTH_REDIRECT_BASE=$(env_quote "$OAUTH_REDIRECT_BASE")
# Web Push (Фаза 7): приватный VAPID-ключ переиспользуется при переустановке.
VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
VAPID_SUBJECT=$(env_quote "$VAPID_SUBJECT")