Обзор
REST JSON API для мобильного приложения IPR SMART. Совместим по маршрутам и формату ответов с legacy old-mobible-api, работает с базой данных ipr_smart.
| Параметр | Значение |
|---|---|
| Базовый URL (dev) | http://localhost:8000 |
| Базовый URL (prod) | https://mobile2.iprbookshop.ru |
| Префикс API | Нет — маршруты в корне (/users/auth, не /api/...) |
| Канонические пути v3 | С префиксом /v3/ |
| Документация | GET /docs |
| Health check | GET /up |
VERSION-API: 3 запросы к legacy-путям (например /books/catalog) автоматически перенаправляются (302) на /v3/books/catalog.
Установка и запуск
composer install
cp .env.example .env # Windows: copy .env.example .env
php artisan key:generate
Настройте .env (см. раздел ниже), затем:
php artisan serve
# → http://localhost:8000
Для production: document root указывает на каталог public/. CI деплоит на mobile2.iprbookshop.ru (master) и mobile.dev.iprbookshop.ru (develop).
Переменные окружения
Для MariaDB/MySQL 5.7 в OSPanel укажите DB_COLLATION=utf8mb4_unicode_ci (MySQL 8 collation utf8mb4_0900_ai_ci не поддерживается).
| Переменная | Обязательна | Описание |
|---|---|---|
DB_* | Да | Подключение к MySQL/MariaDB, база ipr_smart |
DB_COLLATION | Рекомендуется | utf8mb4_unicode_ci для MariaDB 10.x |
SEARCH_API_URL | Да | URL внешнего поискового сервиса (каталоги книг/журналов) |
MOBILE_AUTH_SALT | Да | Соль для токенов и legacy-паролей |
PAGE_SIZE | Нет | Размер страницы пагинации (по умолчанию 50) |
CONTENT_PATH | Да* | Путь к файлам публикаций (PDF/EPUB) |
OAUTH_TOKEN | Для OAuth | Секрет для GET /oauth |
APP_SOURCE_URL | Нет | Базовый URL ссылок на публикации |
APP_DEBUG | Нет | При true отсутствие VERSION-API трактуется как версия 3 |
Заголовки и версии API
Почти все запросы обязаны содержать заголовок:
VERSION-API: 3
Исключение: GET /oauth — заголовок не требуется (как в old-mobible-api).
| Версия | Формат ответа | Авторизация токеном |
|---|---|---|
1 | Legacy (success: "true") | Только uid |
2 | Legacy | uid + deviceId + accessToken + date |
3 | v3 (meta + data) | Как версия 2 + редирект на /v3/ |
4 | как v3 | Как версия 3 |
VERSION-API (при APP_DEBUG=false) возвращает HTTP 404.
Авторизация
Публичные маршруты (с VERSION-API)
Параметры uid / deviceId / accessToken не нужны:
POST /users/authGET /users/statusGET /users/checkGET /users/restorePOST /users/createGET|POST /users/registerGET /users/code/checkGET /users/code/infoGET|POST /users/create/by-codeGET /v3/books/catalog,GET /v3/books/{id}GET /v3/journals/catalog,GET /v3/journals/{id}GET /v3/years/{yearId},GET /v3/numbers/{id}GET /v3/authors,GET /v3/publication-types,GET /v3/education-types,GET /v3/ugnps,GET /v3/scientific-schools,GET /v3/languages,GET /v3/bbks,GET /v3/isbnsGET /v3/journals/years,GET /v3/journals/publishers,GET /v3/journals/ugnps,GET /v3/journals/vak,GET /v3/journals/isbnsGET /v3/home/novelties,GET /v3/home/popular,GET /v3/home/journals
Если в запросе каталога передан верный uid и токен, выдача ограничивается организацией пользователя. Без них каталог общий, избранное в карточке — 0.
Без VERSION-API
GET /oauth
Защищённые маршруты
Middleware ApiAccess проверяет следующие параметры (query string или body):
| Параметр | Обязателен | Описание |
|---|---|---|
uid | Да | Числовой ID пользователя |
deviceId | Да (v2+) | Идентификатор устройства (тот же, что при авторизации) |
accessToken | Да (v2+) | Ежедневный токен (см. формулу ниже) |
date | Нет | Дата в формате Ymd (по умолчанию — сегодня) |
Формула токена доступа
Поток авторизации
Пароли при входе
- Принимается только формат приложения:
md5(md5(plain) + MOBILE_AUTH_SALT) - Legacy MD5 в БД → сравнивается с
md5(хранимый_md5 + соль) - Bcrypt в БД → хеш приложения сверяется с сохранённым верификатором (регистрация, восстановление пароля)
Формат ответов
v3 (VERSION-API: 3)
{
"meta": { "success": true, "message": "" },
"data": { ... }
}
Ошибка:
{
"meta": { "success": false, "message": "Текст ошибки" },
"data": null
}
Legacy (VERSION-API: 1–2)
{ "success": "true", "message": "", ... }
{ "success": "false", "message": "Текст ошибки" }
Авторизация (POST /users/auth) при legacy оборачивает данные в ключ user:
{
"success": "true",
"message": "",
"user": {
"username": "...", "uid": 123, "accessToken": "...",
"cid": 45, "fullname": "...", "org_name": "..."
}
}
Исключения из общих правил
| Эндпоинт | Формат |
|---|---|
GET /users/status | Всегда legacy, даже при VERSION-API: 3 |
GET|POST /users/register, GET|POST /users/create/by-code, GET /users/code/info | { success: bool, message, data?, errors? }. В message — конкретная ошибка, в errors — ошибки полей |
GET /oauth | { success: bool, message, data? } — boolean, не строка |
POST /favorite/uploads с listId | Всегда legacy { success: "true", message } |
| Цитаты, закладки, рейтинг, контент | Собственный { meta, data } без повторной обёртки ApiController |
GET /v3/autocomplete | Всегда v3-обёртка |
Пагинация
{
"meta": { "success": true, "message": "" },
"data": {
"total": 100,
"per_page": 50,
"current_page": 1,
"last_page": 2,
"next_page_url": "...",
"prev_page_url": null,
"from": 1,
"to": 50,
"data": [ ... ]
}
}
Исключение: GET /users/status всегда возвращает legacy-формат независимо от версии API (см. таблицу выше).
Совместимость с old-mobible-api
API реализован как замена legacy old-mobible-api с сохранением маршрутов, имён полей (pagetitle, tv_*) и форматов ответов.
Сводка по группам маршрутов
| Группа | Статус | Примечание |
|---|---|---|
| Users + OAuth | Совпадает | OAuth без VERSION-API; legacy auth → ключ user |
| v3 Books / Journals / Numbers | Совпадает | Legacy-пути редиректятся на /v3/ |
| v3 Search | Частично | Тот же маршрут и параметры; backend — Search API вместо Sphinx |
| Favorites | Совпадает | listId + uploads[]; type 1/2 для номеров |
| Подборки | Как в ipr-smart.local | Таблицы books_selection / selections_books |
| Quotes / Bookmarks / Rating | Совпадает | Цитаты отдают ext_id; закладки без хранения text |
GET /download | Совпадает | XOR-шифрованный PDF, ключ — первые 16 символов device token |
GET /ssl-download | Новый | AES-256-CBC чанками, как ридер ipr-smart.local |
GET /search/prepare | Не реализован | Клиент не вызывает |
GET /v3/ugnps | Справочник УГСН | Старый GET /ugnp/s клиент не вызывает, список берётся из БД |
POST /send/request | Совпадает | Обращение в поддержку |
Legacy GET /search (v1–2) | Частично | Старый формат { total, search[] } с фильтрами не воспроизведён |
Значения selection_options (каталог книг)
| Значение | Фильтр |
|---|---|
1 | Приоритетные (sort_priority = 1) |
2 | Рекомендованные |
3 | Новинки (за 30 дней) |
4 | Популярные (read_count > 200) |
5 | Часто добавляемые в избранное |
Пользователи
Авторизация пользователя и регистрация устройства.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
username | string | Да | Логин или email |
passwd | string | Да | Пароль (см. раздел авторизации) |
device_id | string | Да | Уникальный ID устройства |
Ответ v3 (data):
{
"meta": { "success": true, "message": "" },
"data": {
"username": "user@example.com",
"uid": 123,
"cid": 45,
"fullname": "Иванов И.И.",
"org_name": "Университет",
"email": "user@example.com",
"blockedAfter": 1735689600,
"blockedHash": "abc...",
"accessToken": "device_token_from_db",
"clientLogotype": "https://...",
"clientColor": "ff9900"
}
}
При VERSION-API: 1–2 те же поля находятся внутри ключа user (см. раздел «Формат ответов»).
Проверка статуса подписки и версии приложения.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
uid | int | Да | ID пользователя |
{
"success": "true",
"message": "",
"version": { "version": 13, "strong": true },
"username": "Иванов И.И.",
"org_name": "Университет",
"clientLogotype": "https://...",
"clientColor": "ff9900"
}
Проверка учётных данных приглашённого пользователя (до регистрации). Для уже зарегистрированных учёток используйте POST /users/auth.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
username | string | Да | Логин/email приглашения |
password / passwd | string | Да | Пароль приглашения |
Восстановление пароля — генерирует новый и отправляет на email.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
email | string | Да | Email зарегистрированного пользователя |
Завершение регистрации приглашённого пользователя. Поддерживаются POST и GET (query-параметры, как в legacy-клиентах). Самостоятельная заявка без готовой учётки — /users/register.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
username | string | Да | Логин приглашения |
password / passwd | string | Да | Пароль приглашения |
email | string | Да | Email нового пользователя |
fullname | string | Да | ФИО |
usertype | int | Да | Тип пользователя: 1 — студент, 2 — аспирант, 3 — преподаватель |
user_password | string | Да | Новый пароль (мин. 6 символов) |
Самостоятельная регистрация: создаётся пользователь с user_is_registered = false и заявка организации. Если почта подходит под домен автоодобрения, заявка подтверждается сразу. Иначе библиотекарям уходит уведомление и письмо. Если пароль не передан, он генерируется (8 символов).
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
organization_id | int | Да | Организация, открытая для регистрации и с активной подпиской |
role_id | int | Да | 4 преподаватель, 5 студент, 6 аспирант, 7 сотрудник |
fullname | string | Да | ФИО |
email | string | Да | Email и логин. Должен быть свободен |
password | string | Нет | Минимум 8 символов. Если пусто — генерируется |
comment | string | Нет | Комментарий к заявке |
phone | string | Нет | Телефон |
date_of_birth | date | Нет | Дата рождения, формат Y-m-d |
Успех:
{
"success": true,
"message": "Вы успешно зарегистрировались в системе",
"data": { "user": { "email": "...", "fullname": "...", "role_id": 5 } }
}
Ошибка — конкретный текст в message, ошибки полей в errors:
{
"success": false,
"message": "Пользователь с таким email уже существует",
"errors": { "email": ["Пользователь с таким email уже существует"] }
}
Другие тексты: Регистрация в выбранную организацию недоступна, Допустимые роли: студент, сотрудник, преподаватель или аспирант, Такой логин уже есть в системе, Не задана роль, Не найден запрос на регистрацию, Не удалось сохранить заявку на регистрацию, Некорректная дата рождения.
Проверка кода приглашения до регистрации. Код ищется как {id}-{code}: числовой id записи и код из 5 цифр, например 15-48291. Ответ в формате версии API.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
invite_code | string | Да | Формат ^\d+-\d{5}$ |
Успех: Код был успешно проверен.
Ошибки в message: Не указан код приглашения, Код приглашения некорректного формата, Код не найден, Срок жизни кода истек. Запросите новый в вашей библиотеке, Закончился лимит регистраций кода приглашений.Запросите новый в вашей библиотеке, У данного кода нет привязки к организации, У данного кода приглашения некорректная роль.
Организация и роль кода приглашения. Проверки те же, что у /users/code/check. В каждом объекте только id и name, остальные поля не отдаются.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
invite_code | string | Да | Формат ^\d+-\d{5}$ |
Успех:
{
"success": true,
"message": "Данные кода были успешно получены",
"data": {
"organization": { "id": 45, "name": "Организация" },
"role": { "id": 5, "name": "Студент" }
}
}
Ошибки в message те же, что у /users/code/check.
Регистрация по коду приглашения. Роль, организация и группа берутся из кода, пользователь сразу считается зарегистрированным. Поддерживаются POST и GET. Если пароль не передан, он генерируется (8 символов) и уходит на email.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
invite_code | string | Да | Код в формате {id}-{5 цифр} |
email | string | Да | Email и логин нового пользователя |
fullname | string | Да | ФИО |
password | string | Нет | Минимум 8 символов. Если пусто — генерируется |
comment | string | Нет | Комментарий |
phone | string | Нет | Телефон |
date_of_birth | date | Нет | Дата рождения, формат Y-m-d |
После успеха registration_count кода увеличивается на 1 и пишется использование кода.
Успех: Вы успешно зарегистрировались в системе, в data.user — отправленные поля.
Ошибки в message: Код не найден, Срок жизни кода истек. Запросите новый в вашей библиотеке, Закончился лимит регистраций кода приглашений.Запросите новый в вашей библиотеке, Регистрация по этому коду недоступна, Код приглашения некорректного формата, Пользователь с таким email уже существует, Такой логин уже есть в системе, Не удалось зафиксировать использование кода приглашения, Некорректная дата рождения.
OAuth-авторизация для внешних систем. Заголовок VERSION-API не требуется.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
username | string | Да | Логин/email |
password | string | Да | Пароль (сравнивается как md5(password) с БД) |
time | string | Да | Временная метка |
hash | string | Да | md5(OAUTH_TOKEN + time) |
Книги
Каталог и карточка публичные. Избранное защищённое. Канонический путь: /v3/books/...
Каталог книг организации (через Search API).
| Параметр | Тип | Описание |
|---|---|---|
page | int | Номер страницы (по умолчанию 1) |
ugnp | string | Код УГНП (фильтр, 00 = все) |
title | string | Название |
authors | string | Авторы |
pubhouse | string | Издательство |
year_left | int | Год от |
year_right | int | Год до |
pub_types | string | Типы изданий через ||, напр. учебник||монография |
publication_types_ids | int[] | ID типов изданий. Через запятую или publication_types_ids[] |
authors_ids | int[] | ID авторов из справочника |
purposes_ids | int[] | Тип образования, id из GET /v3/education-types (без вложенных levels). Алиас: education_type |
education_level_ids | int[] | Уровни из ugnp_profile_levels: вложенные levels и пункты с level: true. Алиас: sub_purposes_ids |
ugnps_ids | int[] | ID укрупнённых групп специальностей. Одиночный ugnp по-прежнему принимается |
scientific_school_id | int | ID научной школы |
languages_ids | int[] | ID языков. Алиас одного значения: language_id |
bbks | string[] | Коды ББК, например 32.97 |
bbk_ids | int[] | ID узлов дерева ББК. Разворачиваются в коды листьев |
isbn | string | ISBN, префикс |
sort_by | string | title — по названию, year — по году (новые сверху), id — по добавлению. Алиас: sort. Без параметра порядок прежний |
selection_options | int | Подборка: 1–5 (см. раздел «Совместимость»). Алиас: selected_options |
Элемент data.data[]: id, pagetitle, tv_year, tv_desc, tv_liability, tv_author
Карточка книги с оглавлением, рейтингом и статусом избранного.
| Параметр | Тип | Описание |
|---|---|---|
id | int (path) | ID книги |
Дополнительные поля: isFavorite ("0"/"1"), size, content (оглавление), rating, user_rating
Избранные книги пользователя.
| Параметр | Тип | Описание |
|---|---|---|
page | int | Страница |
last_id | int | ID последней записи (для инкрементальной подгрузки) |
Автодополнение для фильтров каталога.
| Параметр | Тип | Описание |
|---|---|---|
type | int | 13 — книги, 28 — журналы |
field | string | pagetitle, tv_author, tv_pubhouse |
query | string | Строка поиска |
Справочники фильтров книг
Списки для выпадающих фильтров каталога. Значения из ответов передаются в GET /v3/books/catalog параметрами из таблицы выше. Списки authors и scientific-schools постраничные: query, page, per_page.
Авторы. Элемент: id, name. В каталог — authors_ids.
Типы изданий по категориям: id, name, types[] (id, name). В каталог — publication_types_ids.
Типы образования из справочника purposes (категория «Образование»). Уровни — из ugnp_profile_levels, как GET /educational-levels/get на основном сайте: вложены в «Высшее образование», а одноимённые с типом не дублируются. Id уровня в каталог уходит как education_level_ids.
Укрупнённые группы специальностей: id, name, code. В каталог — ugnps_ids.
Научные школы: id, name. В каталог — scientific_school_id.
Языки: id, name. В каталог — languages_ids.
Дерево ББК: id, name, code, children. В каталог можно передать bbk_ids выбранных узлов или готовые bbks (коды).
Подсказки ISBN для поля фильтра. Параметр query — от 3 символов, в ответе до 20 значений { "isbn": "..." }. В каталог выбранное значение уходит как isbn.
Журналы
| Параметр | Тип | Описание |
|---|---|---|
page | int | Страница |
title | string | Название |
year | int | Год выпуска. Равносильно year_left и year_right |
year_left, year_right | int | Диапазон года выпуска |
pubhouse | string | Название издательства |
publishers_ids | int[] | Издательства из GET /v3/journals/publishers |
ugnp | string | Один УГНП (по умолчанию 00 — без фильтра) |
ugnps_ids | int[] | Укрупнённые группы специальностей |
vak | 0|1 | Входит ли в ВАК: 1 — да, 0 — нет. Без параметра — все |
isbn | string | ISBN, поиск по началу |
Элемент data.data[]: id, pagetitle, tv_year, tv_desc, tv_pubhouse, pubhouse, image
Карточка журнала со списком доступных годов.
Поля: id, pagetitle, tv_desc, tv_pubhouse, pubhouse, image, years[] (id, pagetitle)
yearId = periodical_id × 10000 + year. Пример: журнал 100, год 2024 → 100012024
Фильтры журналов
Публичные списки для выпадающих фильтров. Списки книг (/v3/ugnps) для УГСН тоже подходят: GET /v3/journals/ugnps отдаёт тот же справочник.
Годы выпуска номеров, по убыванию. Элемент: id (год), name.
Издательства журналов. Параметры: query, page, per_page. Элемент: id, name. В каталог передаётся publishers_ids.
Укрупнённые группы специальностей: id, name, code. В каталог — ugnps_ids.
Варианты «Входит ли в ВАК»: id=1 «Да», id=0 «Нет».
Подсказки ISBN журналов. Параметр query от 3 символов, до 20 значений. Элемент: isbn.
Избранные номера журналов пользователя (как в старом API). Параметры: page, last_id
Номера журналов
Список номеров журнала за год.
Ответ: id, pagetitle, pagetitle_journal, numbers[]
Карточка номера журнала (аналогично книге: isFavorite, content, rating).
Поиск
Полнотекстовый поиск через Search API.
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
s | string | Да | Поисковый запрос |
type | int | Нет* | 1 — книги, 2 — номера журналов. Обязателен, если не передан id |
page | int | Нет | Страница результатов каталога (по умолчанию 1) |
id | int | Нет | ID издания: искать слово в оглавлении/страницах этой публикации |
Главная
Блоки как на главной ipr-smart.local: новинки, популярные книги и журналы. Публичные. Параметр limit — размер списка (по умолчанию 10, максимум 50).
Новинки: публичные книги за последние 200 дней, язык RU (language_id = 1), по убыванию id.
Элемент как в каталоге книг: id, pagetitle, tv_author, image, …
Популярные книги (is_popular = 1). Формат как у новинок.
Популярные журналы (is_popular = 1), сортировка по последнему году выпуска. Элемент как в каталоге журналов плюс years_range (например 2019–2024).
Последнее чтение текущего пользователя (как GET /statistic/read/last на сайте).
{
"meta": { "success": true, "message": "" },
"data": {
"last": {
"id": 68,
"publication_id": 105644,
"publication_type": 1,
"page": 12,
"publication": { "id": 105644, "title": "...", "title_additional": "", "pagetitle": "...", "authors": "...", "cover": "...", "image": "..." },
"book": { "...": "то же, если publication_type = 1" }
}
}
}
publication_type: 1 — книга, 2 — номер журнала. Если чтений нет или издание удалено — last: null.
Избранное
Тип публикации: type=1 — книга, type=2 — журнал или номер. Если type не указан, тип определяется по ID.
Добавить в избранное.
| Параметр | Тип | Описание |
|---|---|---|
id | int (path) | ID публикации |
type | int | 1 (книга) или 2 (журнал/номер). Если не передан — определяется по ID |
Удалить из избранного. Параметр type — как выше.
Если издания не было в избранном: meta.message = «Издание не находилсь в избранном».
Пакетная синхронизация избранного. Поддерживаются два формата тела запроса.
Legacy (old-mobible-api): параметр listId — массив или одно значение ID книг. Ответ всегда legacy:
listId[]=123&listId[]=456&uid=123
→ { "success": "true", "message": "Издания успешно добавлены!" }
Расширенный формат:
{
"uploads": [
{ "id": 123, "type": 1, "action": "add" },
{ "id": 456, "type": 2, "action": "delete" }
]
}
type: 1 — книга, 2 — номер журнала. action: add или delete.
Подборки
Пользовательские подборки книг — как в личном кабинете ipr-smart.local. Защищённые маршруты. Канонический путь: /v3/selections/... (те же пути есть и без префикса /v3).
Список подборок текущего пользователя.
{
"meta": { "success": true, "message": "Подборки были успешно получены" },
"data": {
"selections": [
{
"id": 1,
"name": "К курсовой",
"first_book_cover": "https://www.iprbookshop.ru/images/books/...",
"books_count": 3
}
]
}
}
| Параметр | Тип | Описание |
|---|---|---|
name | string | Название подборки. Обязателен |
Ответ: data.selection с полями id, name, first_book_cover, books_count.
Одна подборка пользователя. Чужие id → ошибка «Подборка не найдена».
Переименовать. Параметр: name.
Мягкое удаление подборки (deleted_at).
| Параметр | Тип | Описание |
|---|---|---|
page | int | Страница, по умолчанию 1 |
per_page | int | Размер страницы, по умолчанию 20 (макс. 50) |
Ответ: data.books — пагинатор Laravel; элементы в формате каталога книг (pagetitle, tv_*, image).
Пакетное добавление. Параметр: books_ids[] (или books_ids).
Добавить одну книгу. Уже есть в подборке → «Данная книга уже была добавлена в эту подборку».
Удалить книгу из подборки.
Скачивание
Два защищённых метода без префикса /v3. Нужны uid, deviceId и ежедневный accessToken (или Authorization: Bearer …). Ответ — не JSON, а бинарный файл.
| Метод | Шифрование | Для кого |
|---|---|---|
GET /download | XOR (legacy) | Текущие Android/Flutter клиенты с PdfDecryptStream |
GET /ssl-download | AES-256-CBC чанками | Новые клиенты; тот же формат, что web-ридер IPR SMART |
XOR-шифрованный PDF издания. Ключ шифрования — первые 16 символов deviceToken устройства (токен из ответа авторизации, поле устройства access_token). Ежедневный параметр accessToken к шифрованию не относится.
| Параметр | Тип | Описание |
|---|---|---|
id | int | ID издания. Обязателен |
type | string | Где искать файл. 1 или book — книга. 2, number, journal, periodical_number — номер журнала. Без параметра ищутся оба хранилища |
Успех: HTTP 200, Content-Type: application/octet-stream, Content-Disposition: attachment; filename={id}. Тело — зашифрованный PDF той же длины, что исходный файл.
Ошибка: пустое тело и HTTP 404. Так отвечают отсутствие id, ошибка авторизации, отсутствующий файл и отсутствие ключа (токен устройства короче 16 символов или устройство не найдено). JSON при ошибке не возвращается.
GET /v3/{id}/content/download — это оглавление, не скачивание PDF.
SSL-скачивание (для мобильной разработки)
Руководство для разработчика приложения: как скачать и расшифровать PDF через GET /ssl-download.
Те же query-параметры и авторизация, что у /download. Шифрование — AES-256-CBC по чанкам, как в web-ридере ipr-smart.local.
| Параметр | Тип | Описание |
|---|---|---|
id | int | ID издания. Обязателен |
type | string | Как у /download |
uid, deviceId, accessToken, date | — | Стандартная авторизация API |
Что отличается от /download
/download | /ssl-download | |
|---|---|---|
| Алгоритм | XOR с deviceToken | AES-256-CBC, PKCS7 |
| Ключ | Первые 16 символов deviceToken (известен клиенту заранее) | Случайные 32 байта на каждый запрос |
| Где ключ | Локально после auth | Заголовок ответа X-Session-Key |
| Длина тела | = размеру PDF | Больше PDF (заголовки кадров + padding) |
Content-Length | Есть (= размер файла) | Нет — не опирайтесь на него |
Обязательный порядок на клиенте
- Сделать
GET /ssl-download?...с теми же auth-параметрами, что для остальных защищённых методов. ЗаголовокVERSION-API: 3обязателен. - Проверить HTTP-статус:
200— ок;404с пустым телом — ошибка (нет файла / нет доступа / нет устройства). JSON-ошибки не будет. - Сразу прочитать заголовки ответа (до или вместе с телом — ключ в теле не дублируется):
Заголовок Обязателен клиенту Описание X-Session-KeyДа Base64 → ровно 32 байта AES-ключа X-Encryption-ModeРекомендуется Должен быть chunkedX-Chunk-SizeНет Размер plaintext-чанка на сервере (обычно 4194304). На расшифровку не влияет X-IVНет Базовый IV сессии. Для расшифровки не нужен: IV каждого чанка уже лежит в теле - Скачать тело как сырые байты (не UTF-8 string, не JSON).
- Разобрать тело на кадры и расшифровать каждый AES-256-CBC.
- Склеить plaintext-чанки по порядку → это PDF. Сохранить/открыть.
Формат тела (кадр за кадром)
offset 0:
uint8 ivLength // всегда 16
bytes chunkIv[ivLength] // 16 байт
uint32 cipherLength // big-endian (network byte order)
bytes ciphertext[cipherLength]
далее — следующий кадр, пока не кончатся байты
Алгоритм расшифровки одного кадра
key = Base64Decode(response.headers["X-Session-Key"]) // 32 bytes
AES-256-CBC decrypt:
key = key
iv = chunkIv из кадра
data = ciphertext из кадра
padding = PKCS7
результат кадра = plaintext bytes
итог = concat(plaintext_0, plaintext_1, ...)
Библиотеки: Android — javax.crypto.Cipher / AES/CBC/PKCS5Padding (это PKCS7); iOS — CommonCrypto / CryptoKit; Flutter — pointycastle / encrypt с AES-CBC + PKCS7.
Псевдокод парсера
fun decryptSslDownload(body: ByteArray, sessionKeyBase64: String): ByteArray {
key = base64Decode(sessionKeyBase64)
assert key.size == 32
out = empty buffer
i = 0
while i < body.size:
if i + 1 > body.size: error("truncated")
ivLen = body[i] as uint8; i += 1
if ivLen != 16: error("unexpected IV length")
if i + ivLen + 4 > body.size: error("truncated header")
chunkIv = body[i .. i+ivLen); i += ivLen
cipherLen = readUInt32BE(body, i); i += 4
if i + cipherLen > body.size: error("truncated body")
cipher = body[i .. i+cipherLen); i += cipherLen
out.append( aes256CbcDecrypt(cipher, key, chunkIv, PKCS7) )
return out // PDF bytes, обычно начинается с "%PDF-"
}
Важные нюансы для мобильного клиента
- Ключ одноразовый. На каждый запрос новый
X-Session-Key. Нельзя сохранить ключ с прошлого скачивания и применить к новому телу. - Не путать с deviceToken. Здесь ключ не из
access_tokenустройства и не ежедневныйaccessToken. - Заголовки могут резаться прокси. На проде проверьте, что nginx/CDN пропускает
X-Session-Key,X-IV,X-Encryption-Mode,X-Chunk-Size(при необходимостиunderscores_in_headers/ CORSAccess-Control-Expose-Headersдля WebView). - HTTP-клиент: нужен доступ к response headers + raw body. Обычный «скачать в файл по URL» без чтения заголовков не подойдёт — ключ потеряется.
- Прогресс: без
Content-Lengthпроцент загрузки по размеру ответа недоступен; можно показывать индетерминированный прогресс или опираться на известный размер издания с карточки (приблизительно, тело будет больше). - Проверка результата: после расшифровки первые байты должны быть
%PDF. Иначе — битый поток или неверный ключ. - Совместимость со smart: формат кадров тот же, что у web-ридера. Логику можно портировать с
decryptChunked/decryptChunkedFromStreamиз сайта. - Стримить расшифровку можно: накапливать входящие байты, как только собран полный кадр — расшифровать и дописать в файл; не обязательно ждать весь ответ в RAM.
Ошибки
| Ситуация | Что видит клиент |
|---|---|
Нет id, нет файла, ошибка auth, устройство не найдено | HTTP 404, пустое тело |
| Обрезанный ответ / неверный ключ | Ошибка на клиенте при разборе кадров или AES |
Минимальный пример запроса
curl -D - -o book.bin \
"https://mobile2.iprbookshop.ru/ssl-download?id=162291&type=1&uid=UID&deviceId=DEVICE&accessToken=TOKEN" \
-H "VERSION-API: 3"
# Из ответа взять X-Session-Key, затем расшифровать book.bin → PDF
Цитаты, закладки, контент, рейтинг
Маршруты на уровне корня (не внутри /books/). {id} — ID публикации (книги).
Список цитат. Элемент: ext_id, page, text, create_date
| Параметр | Описание |
|---|---|
page_id | Номер страницы |
text | Текст цитаты |
Параметр: quote_id
Обновить цитату. Параметр: text
Список закладок (формат аналогичен цитатам).
Добавить закладку.
| Параметр | Описание |
|---|---|
page_id | Номер страницы (обязателен) |
text | Комментарий (в legacy API; в новой схеме не сохраняется) |
Параметр: bookmark_id
Оглавление публикации. Ответ: [{ "page": 1, "text": "Глава 1" }, ...]
Поиск слова в оглавлении/тексте страниц издания.
| Параметр | Описание |
|---|---|
s | Искомое слово или фраза |
page | Опционально: искать только на этой странице |
Ответ: [{ "page": 15, "text": "..." }, ...]
Установить оценку. Параметр: value (1–5).
{
"meta": { "success": true, "message": "" },
"data": { "value": 4.5, "user_value": 5 }
}
Примеры запросов
1. Авторизация
passwd — не открытый пароль, а md5(md5(plain) + MOBILE_AUTH_SALT):
curl -X POST "http://localhost:8000/users/auth" \
-H "VERSION-API: 3" \
-d "username=user@example.com" \
-d "passwd=COMPUTED_ANDROID_HASH" \
-d "device_id=my-device-001"
2. Вычисление токена (PHP)
$salt = 'Ga90IfDBzo2Mqcva'; // MOBILE_AUTH_SALT
$deviceToken = '...'; // accessToken из ответа auth
$deviceId = 'my-device-001';
$date = date('Ymd');
$token = md5($salt . $deviceToken . $deviceId . $date);
3. Каталог книг
curl "http://localhost:8000/v3/books/catalog?page=1&uid=123&deviceId=my-device-001&accessToken=COMPUTED_TOKEN&date=20250714" \
-H "VERSION-API: 3"
4. Карточка книги
curl "http://localhost:8000/v3/books/42?uid=123&deviceId=my-device-001&accessToken=COMPUTED_TOKEN" \
-H "VERSION-API: 3"
5. Поиск
curl "http://localhost:8000/v3/search?s=математика&type=1&page=1&uid=123&deviceId=my-device-001&accessToken=COMPUTED_TOKEN" \
-H "VERSION-API: 3"
6. Добавить в избранное
curl -X PUT "http://localhost:8000/favorite/42?type=1&uid=123&deviceId=my-device-001&accessToken=COMPUTED_TOKEN" \
-H "VERSION-API: 3"
7. Синхронизация избранного (legacy listId)
curl -X POST "http://localhost:8000/favorite/uploads" \
-H "VERSION-API: 3" \
-d "listId[]=42&listId[]=43&uid=123&deviceId=my-device-001&accessToken=COMPUTED_TOKEN"
8. OAuth (без VERSION-API)
curl "http://localhost:8000/oauth?username=user&password=secret&time=1234567890&hash=COMPUTED_HASH"
Типичные ошибки
| Ситуация | Ответ |
|---|---|
Нет заголовка VERSION-API (кроме /oauth) | HTTP 404 |
Неверный uid | Нет данных о пользователе |
| Неверный токен | Ошибка авторизации |
| Неверный пароль при auth | указан неверный пароль |
| Пустой поисковый запрос | По данному запросу не найдено изданий. |
| Search API недоступен | Ошибка поискового сервиса: ... |
GET /download / GET /ssl-download: нет файла, неверный токен или нет id | HTTP 404, пустое тело |
| Несовместимая collation MySQL | Установите DB_COLLATION=utf8mb4_unicode_ci в .env |
IPR SMART Mobile API · Laravel 11.57.1 · Совместимость с old-mobible-api