<p align="right"><a href="https://github.com/Artasov/orcestr-auth/blob/main/frontend/packages/forms/README.md">English</a> · <strong>Русский</strong></p>

<p align="center"><a href="https://orcestr.com"><img src="https://raw.githubusercontent.com/Artasov/orcestr-auth/main/assets/orcestr-banner.webp" alt="Баннер Orcestr" width="100%" /></a></p>

# @orcestr/auth-forms

[![npm](https://img.shields.io/npm/v/@orcestr/auth-forms)](https://www.npmjs.com/package/@orcestr/auth-forms)
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://github.com/Artasov/orcestr-auth/blob/main/LICENSE)

Готовые формы авторизации на [`@orcestr/ui`](https://github.com/Artasov/orcestr-ui).
Пакет экспортирует формы, но не страницы, routes, metadata и product branding.

## Установка

```bash
npm install @orcestr/auth-core @orcestr/auth-react @orcestr/auth-forms
npm install @orcestr/ui @tanstack/react-query react react-dom react-icons
```

## Формы

- `LoginForm`
- `RegisterForm`
- `ForgotPasswordForm`
- `ResetPasswordForm`
- `VerifyEmailForm`
- `ChangePasswordForm`
- `OAuthButtons`

## Локализация

Полные английский и русский словари встроены в пакет. Приложение выбирает locale и может
переопределять product wording без копирования форм:

```tsx
import { AuthI18nProvider, LoginForm } from "@orcestr/auth-forms";

<AuthI18nProvider locale="ru">
  <LoginForm
    next="/dashboard"
    registerHref="/register?next=%2Fdashboard"
    onSuccess={(user) => router.replace("/dashboard")}
  />
</AuthI18nProvider>;
```

Сообщения выбираются по стабильным API error codes вроде `invalid_credentials`, а не по
английскому fallback-тексту сервера. Поэтому смена locale применяется и к validation/request
errors, и к подписям формы.

Формы принимают callbacks, ссылки, slots и product extensions, включая registration
`extraPayload` и `legalContent`. Consumer собирает страницы из семантического HTML и
существующих layout primitives `@orcestr/ui`; application routes и surface-aware navigation
остаются локальными.

## Компоненты OAuth-кнопок

### Загрузка способов входа

Получайте методы через headless-хук и передавайте состояние в форму:

```tsx
import { useAuthMethods } from "@orcestr/auth-react";
import { LoginForm } from "@orcestr/auth-forms";

function Login() {
  const methods = useAuthMethods();
  return <LoginForm
    methods={methods.data}
    methodsPending={methods.isPending}
    methodsError={methods.error}
    onRetryMethods={() => void methods.refetch()}
  />;
}
```

RegisterForm принимает те же props. Во время загрузки/ошибки формы показывают локализованный
статус или кнопку повтора и блокируют отправку/OAuth, а не молча скрывают недоступные методы.
`AuthMethodsStatus` также экспортируется для собственных композиций форм.

Провайдер виден, только если разрешён в `allowed_oauth_providers` и имеет непустой
`oauth_client_ids[provider]`. Разрешённый провайдер без credentials намеренно скрыт.
Ошибка `/auth/methods/` — отдельное состояние, а не пустой список провайдеров.

Для локальных поддоменов вроде `http://deliveries.localhost:8934` backend должен включать
`OAuthRedirectPolicy(allow_localhost=True)` только в development. Production origins задаются
явно. Сам OAuth-провайдер тоже должен принимать точный callback URI: разрешение origin в
библиотеке не регистрирует адрес в консоли Google/GitHub.

По умолчанию OAuth-провайдеры отображаются стандартными полноразмерными кнопками. Продукт может
одним компонентом заменить все кнопки, отдельно переопределить нужных провайдеров и выбрать
компоновку группы, не копируя логику авторизации:

```tsx
import { LoginForm, type OAuthProviderButtonProps } from "@orcestr/auth-forms";
import { IconButton, Tooltip } from "@orcestr/ui";
import { FcGoogle } from "react-icons/fc";

function GoogleButton({ label, onClick }: OAuthProviderButtonProps) {
  return (
    <Tooltip content={label}>
      <IconButton type="button" aria-label={label} onClick={onClick} round>
        <FcGoogle />
      </IconButton>
    </Tooltip>
  );
}

<LoginForm
  methods={methods}
  oauthButtons={{
    placement: "after-submit",
    direction: "row",
    justify: "center",
    buttonComponents: { google: GoogleButton },
  }}
/>;
```

Компонент получает провайдера, локализованную доступную подпись и готовое действие `onClick`.
Client ID, PKCE, state, redirect URI и навигация остаются внутри библиотеки.

`placement` принимает `before-fields`, `after-submit` или `after-links` и одинаково работает в
формах входа и регистрации.

### Custom OAuth transport и provider hints

Web default по-прежнему собирает URL провайдера и присваивает его `window.location.href`. Desktop
и другие hosted surfaces могут заменить только transport, сохранив те же кнопки и orchestration
legal-действия:

```tsx
<LoginForm
  methods={methods}
  oauthLegalConsent={false}
  oauthButtons={{
    autoAuthorizeProvider: providerHint,
    authorizeHandler: (provider, clientId, next, callbackPayload) =>
      nativeAuthorize({ provider, clientId, next, callbackPayload }),
  }}
/>
```

`autoAuthorizeProvider` запускается не более одного раза на mounted группу кнопок и только после
того, как запрошенный provider одновременно появился в `methods.allowed_oauth_providers` и получил
непустой client ID. Пока форма disabled, запуск ожидает. Так trusted provider hint не может начать
авторизацию через недоступного провайдера.

По умолчанию OAuth использует gate `legalConsent`, как и раньше. Передавайте
`oauthLegalConsent={false}` только когда внешний authorization flow показывает и сохраняет те же
документы; password login продолжает использовать настроенный legal gate.

## Версионированное принятие документов

`LoginForm` и `RegisterForm` могут защищать password- и OAuth-действия общей настраиваемой
модалкой юридических документов. Сами документы принадлежат приложению и могут загружаться из БД;
пакет отвечает за отображение, обязательные и необязательные галочки, продолжение действия и
перенос payload через OAuth callback state.

```tsx
<LoginForm
  legalConsent={{
    selectAllOnFirstDocumentCheck: true,
    storage: { key: "my-product:auth-legal-consent" },
    documents: legalDocuments.map((document) => ({
      id: document.slug,
      title: document.title,
      version: document.version,
      href: `/legal/${document.slug}`,
      required: document.required,
      acceptance: {
        document_slug: document.slug,
        version: document.version,
        language: document.language,
      },
    })),
  }}
/>
```

Для отключения gate используется `enabled: false`. Если backend ожидает другой контракт, можно
задать `payloadKey` или `buildPayload`. Количество обязательных и необязательных документов не ограничено.

`selectAllOnFirstDocumentCheck: true` превращает первую галочку в компактное управление всем
списком: при установке выбираются все документы, при снятии очищается весь выбор. Если каждое
согласие должно устанавливаться независимо, параметр указывать не нужно.

Принятые пары `id` и `version` по умолчанию сохраняются в `localStorage` браузера. Если версии всех
текущих обязательных документов уже сохранены, модальное окно не показывается, но актуальные
документы всё равно добавляются в payload входа, регистрации или OAuth. Новый обязательный
документ или новая версия обязательного документа снова откроют окно; ещё актуальные ранее
выбранные пункты будут восстановлены. Изменение необязательного документа само по себе не требует
повторного подтверждения.

Для разделения нескольких политик на одном origin задайте
`storage: { key: "your-product:legal-consent" }`. `storage: false` полностью отключает сохранение.
Ключ по умолчанию — `@orcestr/auth-forms:legal-consent`. Запись в браузере нужна только для удобства
пользователя: она не является серверным аудитом или границей безопасности. Backend должен
по-прежнему проверять и сохранять версии документов из отправленного payload.
