# Приёмка liderra.ru — ПУНКТ №26: Публичный API сделок + API-ключи (G6)

> **Для исполнителя:** формат PR1 (одобрен 20.06). Каждый шаг = `🔧 Код-факт (file:line)` · `🎬 Действие` · `📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)` · `❌ Если не так`. Техника «под капотом»; владельцу — человеческие карточки.

**Цель:** показать, что клиент может завести **ключ для интеграций** и по нему его программы автоматически забирают **только свои** сделки. Ключ показывается **один раз** (как пароль), при перевыпуске старый сразу перестаёт работать, живёт год. По ключу отдаются только свои сделки; без ключа/с чужим/просроченным — доступа нет.

**Что снимаем глазами:** **Настройки → API и Webhook** (создание/копирование/маска ключа), ответ API (JSON), сверка чисел со **Сделками**. Живые 📸.

**Источник сценария:** свод проверок №26 (G6); новая фича деплоя 19.06; углубление карточки 8 эфира.

---

## 🔧 Код-факты (подтверждено чтением 20.06)

- **Ключ показывается один раз** — `regenerate` возвращает `key` (полный `lpkapi_…`) в ответе; в базе — только `key_prefix` (первые 10) + bcrypt-хеш — [ApiKeyController.php:58-83](../../../app/app/Http/Controllers/Api/ApiKeyController.php#L58-L83), префикс `lpkapi_` — [:25](../../../app/app/Http/Controllers/Api/ApiKeyController.php#L25).
- **Один активный ключ** — перевыпуск деактивирует прежние — [:47-51](../../../app/app/Http/Controllers/Api/ApiKeyController.php#L47-L51).
- **TTL 365 дней, scope `read`** — [:61-62](../../../app/app/Http/Controllers/Api/ApiKeyController.php#L61-L62) (`expires_at = now()->addYear()`, `scopes=['read']`).
- **Аутентификация по ключу** — нет ключа → **401**; неактивный/просроченный/битый → **401**; нет scope `read` → **403**; успех → `last_used_at`/`last_used_ip` + tenant в контекст — [ApiKeyAuth.php:27-56](../../../app/app/Http/Middleware/ApiKeyAuth.php#L27-L56).
- **API сделок** — `GET /api/v1/deals`: только свои (`where tenant_id` + RLS), cursor-пагинация (limit 1..500), фильтр `since` — [V1/DealsController.php:21-65](../../../app/app/Http/Controllers/Api/V1/DealsController.php#L21-L65).
- ⚠️ **Rate-limit пока НЕТ** (по своду) — отметить как ограничение.

---

## Шаг 26-1 — Создать/перевыпустить ключ → показан один раз

**🔧 Код:** [regenerate :42-83](../../../app/app/Http/Controllers/Api/ApiKeyController.php#L42-L83).
**🎬 Действие (лок):** Настройки → «API и Webhook» → перевыпустить ключ.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Ключ | нет | создан, показан **один раз** (`lpkapi_…`) | _(слот прогона)_ | 📸 «БЫЛО» (нет ключа) и «СТАЛО» (ключ показан) |
| После закрытия | — | виден только префикс/маска (полный не повторить) | _(слот)_ | 📸 маска ключа |
| Старый ключ | был активен | при перевыпуске перестаёт работать | _(слот)_ | 📝 один активный ключ |

**💡 Что внутри:** клиент заводит себе ключ-пароль для интеграций. Портал показывает его **один раз** — надо сразу сохранить; потом видна только маска. Если перевыпустить — старый ключ мгновенно отключается (как смена пароля). Ключ действует год.

**❌ Если не так:** ключ показывается повторно / старый продолжает работать → дефект безопасности.

---

## Шаг 26-2 — По ключу приходят ТОЛЬКО свои сделки

**🔧 Код:** [DealsController::index :21-65](../../../app/app/Http/Controllers/Api/V1/DealsController.php#L21-L65); auth — [ApiKeyAuth :40-56](../../../app/app/Http/Middleware/ApiKeyAuth.php#L40-L56).
**🎬 Действие (лок):** запрос `GET /api/v1/deals` с `Authorization: Bearer lpkapi_…`; сверить число с разделом Сделки.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Ответ API | — | JSON со списком **своих** сделок (id, дата, телефон, контакт, город, статус, проект) | _(слот)_ | 📝 ответ API (JSON) |
| Число сделок | — | = числу в кабинете (его 10) | _(слот)_ | 📸 Сделки UI + 📝 ответ API |
| Чужие | — | **нет** (только свой tenant) | _(слот)_ | 📝 все строки своего tenant |

**💡 Что внутри:** программа клиента шлёт запрос с его ключом — и получает ровно его сделки, в удобном машинном виде. Чужого по ключу не достать.

**❌ Если не так:** в ответе чужие сделки / число ≠ UI → утечка/дефект.

---

## Шаг 26-3 — Постраничность и фильтр по дате (внутр.)

**🔧 Код:** cursor/since — [DealsController :25-65](../../../app/app/Http/Controllers/Api/V1/DealsController.php#L25-L65).
**🎬 Действие (лок):** запросить с `limit` и `cursor` (постранично), затем `since=<дата>`.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Постранично | — | `next_cursor` ведёт к следующей пачке (limit 1..500) | _(слот)_ | 📝 ответ с next_cursor |
| Фильтр `since` | — | только сделки с даты | _(слот)_ | 📝 ответ ограничен датой |

**❌ Если не так:** cursor/since не работают → дефект пагинации/фильтра.

---

## Шаг 26-4 — Без ключа / битый / просроченный / нет прав

**🔧 Код:** [ApiKeyAuth :27-51](../../../app/app/Http/Middleware/ApiKeyAuth.php#L27-L51).
**🎬 Действие (лок):** запрос без ключа; с неверным ключом; (если возможно) с просроченным; с ключом без scope `read`.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Без ключа | — | **401** «требуется ключ» | _(слот)_ | 📝 ответ 401 |
| Битый/просроченный | — | **401** | _(слот)_ | 📝 ответ 401 |
| Нет scope read | — | **403** «недостаточно прав» | _(слот)_ | 📝 ответ 403 |
| Успех | — | пишется `last_used_at`/`ip` | _(слот)_ | 📝 поля использования |

**💡 Что внутри:** без правильного ключа доступа нет вообще. Просроченный или поддельный ключ не пускают. Портал ещё и запоминает, когда и с какого адреса ключом пользовались — для контроля.

**❌ Если не так:** без ключа/с битым пускает → дыра доступа (СТОП).

---

## Verification №26 (выход из пункта)
- [ ] 📸 Ключ создаётся, показан один раз; потом маска; старый отключается.
- [ ] 📸/📝 По ключу — только свои сделки; число = UI.
- [ ] 📝 Постранично (cursor) и фильтр `since` работают.
- [ ] 📝 Без/битый/просроченный ключ → 401; нет scope → 403; пишется last_used.
- [ ] 📝 Отметить: rate-limit пока НЕТ (ограничение).

## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** Настройки → API (создание/маска ключа), Сделки UI для сверки числа.
- **📝 Текст:** ответы API (JSON, 401/403), cursor/since, last_used — текстом.

## Грабли
- Ключ показывается один раз — на прогоне сразу сохранить значение для запросов.
- TTL 365д — просроченный кейс смоделировать сложно; проверять хотя бы 401 на битом.
- **Rate-limit отсутствует** — зафиксировать как известное ограничение (возможная будущая находка).
- Связка с №16 (изоляция) — API-доступ тоже только свой.
