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:
@@ -5,7 +5,6 @@ import { Link, useLocation, useNavigate, useSearchParams } from 'react-router';
|
||||
|
||||
import { login } from '@/api/auth';
|
||||
import { fetchMeta, metaQueryKey } from '@/api/meta';
|
||||
import { fetchOAuthProviders, oauthStartURL } from '@/api/oauth';
|
||||
import { loginWithPasskey } from '@/api/passkeys';
|
||||
import { ErrorNotice } from '@/components/ui/ErrorNotice';
|
||||
import { Field } from '@/components/ui/Field';
|
||||
@@ -46,13 +45,6 @@ export default function LoginPage() {
|
||||
const stateFrom = (location.state as LocationState | null)?.from ?? null;
|
||||
const from = next !== null && next.startsWith('/') ? next : (stateFrom ?? '/app');
|
||||
|
||||
const oauthReturned = searchParams.get('oauth');
|
||||
const oauthError = searchParams.get('oauth_error');
|
||||
const providers = useQuery({
|
||||
queryKey: ['auth', 'oauth', 'providers'],
|
||||
queryFn: ({ signal }) => fetchOAuthProviders(signal),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
// Признак доступности passkeys берём из меты инстанса: на IP-адресе (стенд
|
||||
// без домена) WebAuthn невозможен, и кнопку показывать нельзя.
|
||||
const meta = useQuery({
|
||||
@@ -97,24 +89,8 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
const registrationEnabled = instance.data?.registration_enabled === true;
|
||||
const providerList = providers.data?.providers ?? [];
|
||||
|
||||
// Ошибка возврата от провайдера: показываем человеческий текст.
|
||||
const oauthErrorMessage =
|
||||
oauthError === null
|
||||
? null
|
||||
: t(
|
||||
oauthError === 'provider_not_configured'
|
||||
? 'auth.login.oauthNotConfigured'
|
||||
: oauthError === 'email_unverified'
|
||||
? 'auth.login.oauthEmailUnverified'
|
||||
: oauthError === 'email_missing'
|
||||
? 'auth.login.oauthEmailMissing'
|
||||
: oauthError === 'user.banned'
|
||||
? 'errors.user.banned'
|
||||
: 'auth.login.oauthFailed',
|
||||
);
|
||||
|
||||
// Ошибка клиента WebAuthn важнее серверной: она объясняет, что делать.
|
||||
const passkeyErrorKey =
|
||||
passkey.error instanceof WebAuthnClientError && passkey.error.code === 'cancelled'
|
||||
@@ -131,17 +107,6 @@ export default function LoginPage() {
|
||||
</header>
|
||||
|
||||
<Card>
|
||||
{oauthReturned === 'ok' ? (
|
||||
<p className="mb-4 text-sm text-success" role="status">
|
||||
{t('auth.login.oauthDone')}
|
||||
</p>
|
||||
) : null}
|
||||
{oauthErrorMessage === null ? null : (
|
||||
<p className="mb-4 text-sm text-danger" role="alert" data-testid="oauth-error">
|
||||
{oauthErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<form className="flex flex-col gap-4" onSubmit={onSubmit} noValidate>
|
||||
<Field
|
||||
label={t('auth.login.email')}
|
||||
@@ -209,24 +174,6 @@ export default function LoginPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{providerList.length === 0 ? null : (
|
||||
<div className="mt-4 flex flex-col gap-2 border-t border-border/60 pt-4">
|
||||
<p className="text-xs text-fg-muted">{t('auth.login.oauthHint')}</p>
|
||||
{providerList.map((provider) => (
|
||||
<Button
|
||||
key={provider.id}
|
||||
variant="ghost"
|
||||
data-testid={`oauth-${provider.id}`}
|
||||
onClick={() => {
|
||||
window.location.assign(oauthStartURL(provider.id, from));
|
||||
}}
|
||||
>
|
||||
{t('auth.login.oauthSubmit', { provider: provider.name })}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-sm text-fg-muted">
|
||||
{t('auth.login.noAccount')}{' '}
|
||||
{registrationEnabled ? (
|
||||
|
||||
Reference in New Issue
Block a user