> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Миграция пользователей: импорт паролей и MFA

> Импорт в Didit пользователей с паролями, почтами, соцсетями, аутентификаторами и ролями организации из CSV, JSON или пакетно.

> [!WARNING]
>
> Authentication является новым. Сначала выполните каждый импорт в песочнице приложения, а затем импортируйте данные в продакшн-приложение.

Импорт переносит пользователей из другой платформы идентификации в Didit Authentication. Вместе с ними переходят их пароль, подтверждённый email, ссылки на вход через соцсети, приложение-аутентификатор и роли в организациях. Для пользователей ничего не меняется, пока ваше приложение не выполнит вход через Didit.

Импорты выполняются через API управления Authentication вашего приложения с сессией в консоли, которая имеет разрешение `write:authentication` (или `read:authentication`, чтобы получать список импортов). Они никогда не используют токен конечного пользователя или API-ключ верификации. В примерах `MANAGEMENT_BASE` выглядит так:

```text theme={null}
https://apx.didit.me/auth/v2/organizations/{org_id}/applications/{application_id}/authentication/
```

## Два способа импорта

| | Задача импорта файла | Пакетный запрос |
| - | - | - |
| endpoint | `POST import-jobs/` (multipart) | `POST imports/` (JSON) |
| Размер | Один файл CSV или JSON до 25 МБ и 100 000 пользователей | От 1 до 1 000 `rows` в одном запросе |
| Пробный прогон | Всегда выполняется первым, в фоновом режиме | `dry_run: true` по умолчанию |
| Фиксация | `POST import-jobs/{id}/start/` | Тот же запрос с `dry_run: false` |
| Частичный сбой | Каждый пакет по 1 000 пользователей фиксируется отдельно; прерванный импорт продолжается с места остановки | Всё или ничего |

## Импорт файла

### 1. Загрузка

```bash theme={null}
curl --fail-with-body "${MANAGEMENT_BASE}import-jobs/" \
  -H "Authorization: Bearer $CONSOLE_ACCESS_TOKEN" \
  -F file=@users.csv \
  -F passwords=import \
  -F keep_social=true \
  -F keep_verified=true \
  -F keep_mfa=true \
  -F keep_ids=true
```

| Поле | Значение |
| - | - |
| `file` | CSV или JSON в кодировке UTF-8 (метка порядка байтов допускается), до 25 МБ. |
| `passwords` | `import` (по умолчанию) сохраняет хеши паролей. `reset` их игнорирует, поэтому все пользователи задают новый пароль. |
| `keep_social` | Сохранить ссылки на вход через соцсети. |
| `keep_verified` | Сохранить `email_verified`. При `false` все адреса email будут неподтверждёнными. |
| `keep_mfa` | Сохранить секреты приложений-аутентификаторов. |
| `keep_ids` | Сохранить `external_id`. При `false` `vendor_data` становится идентификатором личности в Didit. |
| `firebase_signer_key`, `firebase_salt_separator`, `firebase_rounds`, `firebase_mem_cost` | Параметры хеширования вашего проекта Firebase. Передавайте все четыре параметра или ни одного. |

Каждый переключатель `keep_*` имеет значение `true`, если вы не передали `false`. Вызов возвращает `202` и задачу. В одном приложении одновременно может храниться не более 5 загруженных файлов.

### 2. Проверка пробного прогона

Каждая загрузка начинается с пробного прогона. Он проверяет каждого пользователя и ничего не записывает. Опрашивайте `GET import-jobs/{id}/`:

| Поле | Значение |
| - | - |
| `kind` | `dry_run`, а после запуска импорта `import`. |
| `status` | `queued`, `running`, затем `passed` или `failed` для пробного прогона, `done` или `error` для импорта. |
| `processed`, `total` | Прогресс в пользователях. |
| `result` | Содержимое файла (см. [результаты](#результаты)). |
| `startable` | `true`, если пробный прогон прошёл успешно и срок хранения файла ещё не истёк. |
| `expires_at` | Файл удаляется через 24 часа после пробного прогона, если файл так и не был импортирован. |

Если пробный прогон завершился с ошибкой, в ответе перечислены ошибки. Исправьте файл и загрузите его заново.

### 3. Запуск импорта

`POST import-jobs/{id}/start/` импортирует данные, которые прошли пробный прогон, и возвращает `202`.

* Пользователи импортируются пакетами по 1 000. Каждый пакет фиксируется отдельно.
* В одном приложении одновременно выполняется только один импорт (иначе возвращается `import_running`).
* Прерванный импорт продолжается после последнего зафиксированного пакета.
* Пользователи, которые уже импортированы, пропускаются, поэтому один и тот же файл можно запустить повторно.
* Тот, кто запустил импорт, получит письмо по его завершении или остановке.

Загруженный файл хранится в зашифрованном виде. Он удаляется по завершении импорта, при сбое пробного прогона или через 24 часа после пробного прогона, если файл так и не был импортирован.

## Пакетный импорт

Отправляйте до 1 000 пользователей в одном запросе с `idempotency_key` длиной до 100 символов.

```json theme={null}
{
  "rows": [
    {
      "email": "user@example.com",
      "external_id": "user-1234",
      "email_verified": true,
      "display_name": "Example User",
      "password_hash": "$2b$12$...",
      "social": [{ "provider": "google", "subject": "1234567890" }],
      "organizations": [{ "slug": "acme", "name": "Acme", "role": "admin" }]
    }
  ],
  "dry_run": true,
  "idempotency_key": "migration-batch-0001"
}
```

* Пробный прогон возвращает `200` с результатом и ничего не записывает.
* При `dry_run: false` успешный импорт возвращает `201`. Если в пакете есть хотя бы одна ошибка, возвращается `400`, и ни один пользователь не импортируется.
* Повторный запрос с уже зафиксированным ключом возвращает сохранённый результат. Тот же ключ с другим содержимым возвращает `400 {"idempotency_key": "different_payload"}`.
* Параметры Firebase передаются в объекте `firebase`: `{signer_key, salt_separator, rounds, mem_cost}`.
* `GET imports/` показывает список зафиксированных пакетов.

## Формат файла

### JSON

Массив пользователей или объект, в поле `users` которого находится массив.

| Поле | Правило |
| - | - |
| `email` | Обязательно. |
| `external_id` | До 255 символов. Становится [`vendor_data`](https://docs.didit.me/ru/entities/vendor-data-linking) пользователя. |
| `email_verified` | `true` или `false` (также `1`/`0`, `yes`/`no`). |
| `display_name` | До 200 символов. |
| `password_hash`, `password_salt`, `password_algorithm` | См. [пароли](#пароли). |
| `totp_secret` | Секрет приложения-аутентификатора в формате Base32. |
| `created_at`, `last_signed_in_at` | ISO 8601. Время без часового пояса считается UTC. |
| `social` | До 8 объектов `{provider, subject, tenant_id}`. `provider` может быть `google`, `apple`, `github` или `microsoft`. `tenant_id` обязателен только для Microsoft. |
| `organizations` | До 20 объектов `{slug, name, role}`. `role` может быть `admin`, `member` (по умолчанию) или `billing`. |

### CSV

Те же столбцы, содержащие одно значение, а также `google_id`, `apple_id`, `github_id`, `microsoft_id`, `microsoft_tenant_id`, `organization_slug`, `organization_name` и `organization_role`. Для пользователя, состоящего в нескольких организациях, указывается отдельная строка для каждой организации.

```csv theme={null}
email,external_id,email_verified,password_hash,google_id,organization_slug,organization_role
user@example.com,user-1234,true,$2b$12$...,1234567890,acme,admin
```

Неизвестные поля или столбцы считаются ошибками и никогда не пропускаются.

### Дубликаты и существующие аккаунты

* Строки с одинаковым email становятся одним пользователем. Полностью совпадающая строка учитывается как дубликат. Строка с большим количеством ссылок, организаций или приложением-аутентификатором объединяется.
* Два разных хеша пароля, секрета аутентификатора или роли в организации у одного пользователя считаются ошибками. Ошибкой также считается `external_id` или идентификатор субъекта провайдера, указанный двумя разными пользователями.
* Если аккаунт с таким email уже существует и его создал предыдущий импорт, пользователь пропускается (`already_imported`). Если пользователь зарегистрировался самостоятельно, возникает ошибка `identity_conflict`. Импорт никогда не изменяет существующий аккаунт.

## Пароли

| Формат | Допустимые значения |
| - | - |
| bcrypt | `$2a$`, `$2b$` или `$2y$`, стоимость от 4 до 13. |
| Argon2id | `$argon2id$v=19$...`. Память до 64 MiB, произведение памяти на проходы до 256 MiB, до 8 потоков. |
| Django PBKDF2-SHA256 | `pbkdf2_sha256$...`, от 10 000 до 2 000 000 итераций. |
| Django scrypt | `scrypt$...`, фактор нагрузки до 32,768, размер блока от 1 до 8, параллелизм от 1 до 5. |
| Firebase scrypt | `password_algorithm: "firebase_scrypt"`, base64 `password_hash` и `password_salt` для каждого человека, а также `signer_key`, `salt_separator`, `rounds` (от 1 до 8) и `mem_cost` (от 1 до 14) вашего проекта, один раз на импорт. |

Параметры Firebase хранятся в зашифрованном виде и только для этого приложения. Любой другой формат или параметр стоимости выше этих пределов означает, что человеку нужно задать новый пароль. После первого успешного входа Didit сохраняет пароль человека заново в своём формате.

### Люди без пригодного пароля

Если у человека нет хеша или хеш не поддерживается, попытка входа с паролем получает обычный ответ о неверном пароле. Didit также отправляет ему на email ссылку для установки нового пароля (действует 30 минут, не чаще одного раза в день). Это происходит только в том случае, если его email подтверждён, аккаунт не заблокирован и вход по паролю включён.

В результате `reset_required` считает людей без пригодного пароля. `reset_emails` считает среди них тех, у кого email подтверждён: только до них можно дотянуться таким способом. Люди с неподтверждённым email не могут войти по паролю. Они по-прежнему могут войти по коду из email, если вы включите этот способ.

## Социальные связи, аутентификаторы и организации

* **Социальные связи** сохраняются для провайдеров, которые ваше приложение уже настроило, с его текущим идентификатором клиента. Повторный вход в этом случае не требует подтверждения по email. Идентификаторы (subject) Google и GitHub глобальны. Идентификаторы Apple привязаны к команде разработчиков, а идентификаторы Microsoft к регистрации приложения, поэтому такие связи совпадают только в том случае, если ваша старая платформа использовала ту же команду или ту же регистрацию. Связи с провайдерами, которых вы не настроили, учитываются как `social_unavailable` и не импортируются.
* **Приложения-аутентификаторы** (секреты TOTP: SHA-1, 6 цифр, 30 секунд) хранятся в зашифрованном виде. Импортированные люди должны использовать их как второй фактор. Коды восстановления для них не создаются.
* **Организации**, которых ещё нет, создаются по их слагу.
* **Ключи доступа** импортировать нельзя. Они привязаны к домену, использовавшемуся для старого входа, поэтому люди добавляют новый ключ доступа после входа через Didit.

## Результаты

Каждый пробный запуск, пакет и задача возвращает один и тот же `result`:

| Поле | Значение |
| - | - |
| `rows`, `people_total` | Прочитанные строки и уникальные люди. |
| `ready`, `already_imported`, `duplicates`, `merged`, `skipped` | Что происходит с каждым человеком. |
| `passwords` | Количество по форматам: `bcrypt`, `argon2id`, `firebase_scrypt`, `pbkdf2_sha256`, `scrypt`. |
| `reset_required`, `reset_emails`, `unsupported_hashes` | Люди, которым нужен новый пароль. |
| `verified_emails` | Импортированные люди с подтверждённым email. |
| `social_links`, `social_unavailable` | Связи, сохранённые по провайдерам, и пропущенные связи. |
| `authenticator_apps` | Люди с импортированным аутентификатором. |
| `organizations` | `count`, `members` и до пяти `names`. |
| `error_count`, `errors` | До 100 ошибок, каждая с нулевым индексом `row` (строка CSV: `row + 2`), полем `field` и кодом ошибки `error`. |

Ответы никогда не содержат хешей паролей, солей, секретов аутентификаторов или значений полей.

| Ошибка | Значение |
| - | - |
| `file_too_large`, `too_many_rows`, `empty_file` | Больше 25 MB, больше 100 000 человек или файл пустой. |
| `invalid_encoding`, `invalid_json`, `invalid_csv` | Файл не удаётся прочитать. |
| `unknown_column`, `missing_column`, `duplicate_column`, `unknown_field` | Файл не соответствует формату. |
| `required`, `invalid_field`, `invalid_row` | Значение отсутствует или недопустимо. |
| `identity_conflict` | Аккаунт с этим email зарегистрировался самостоятельно. |
| `social_link_conflict`, `password_conflict`, `authenticator_conflict`, `role_conflict` | Две строки для одного человека расходятся. |
| `too_many_uploads`, `import_running`, `not_startable`, `file_expired` | Задачу сейчас нельзя запустить. |

## Перед переключением

Проверьте в своём приложении в песочнице:

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

Затем убедитесь, что `vendor_data` каждого человека совпадает со значением, которое вы используете для его [верификаций и транзакций](https://docs.didit.me/ru/authentication/overview.md#один-человек-в-разных-продуктах).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.