Обзор

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 checkGET /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).

ВерсияФормат ответаАвторизация токеном
1Legacy (success: "true")Только uid
2Legacyuid + deviceId + accessToken + date
3v3 (meta + data)Как версия 2 + редирект на /v3/
4как v3Как версия 3
Неверный или отсутствующий VERSION-API (при APP_DEBUG=false) возвращает HTTP 404.

Авторизация

Публичные маршруты (с VERSION-API)

Параметры uid / deviceId / accessToken не нужны:

Если в запросе каталога передан верный uid и токен, выдача ограничивается организацией пользователя. Без них каталог общий, избранное в карточке — 0.

Без VERSION-API

Защищённые маршруты

Middleware ApiAccess проверяет следующие параметры (query string или body):

ПараметрОбязателенОписание
uidДаЧисловой ID пользователя
deviceIdДа (v2+)Идентификатор устройства (тот же, что при авторизации)
accessTokenДа (v2+)Ежедневный токен (см. формулу ниже)
dateНетДата в формате Ymd (по умолчанию — сегодня)

Формула токена доступа

accessToken = md5(MOBILE_AUTH_SALT + deviceToken + deviceId + date) где: deviceToken — значение accessToken из ответа POST /users/auth deviceId — идентификатор устройства date — Ymd, например 20250714

Поток авторизации

1. POST /users/auth → получить uid, accessToken (deviceToken) 2. Вычислить дневной токен по формуле выше 3. Передавать uid, deviceId, accessToken, date на все защищённые запросы

Пароли при входе

Формат ответов

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_*) и форматов ответов.

v3 (основной сценарий мобильного приложения) — покрытие ~95%: пользователи, каталоги, поиск, избранное, цитаты, закладки, рейтинг.

Сводка по группам маршрутов

ГруппаСтатусПримечание
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Часто добавляемые в избранное

Пользователи

POST /users/auth Публичный

Авторизация пользователя и регистрация устройства.

ПараметрТипОбязателенОписание
usernamestringДаЛогин или email
passwdstringДаПароль (см. раздел авторизации)
device_idstringДаУникальный 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 (см. раздел «Формат ответов»).

GET /users/status Публичный

Проверка статуса подписки и версии приложения.

ПараметрТипОбязателенОписание
uidintДаID пользователя
{
  "success": "true",
  "message": "",
  "version": { "version": 13, "strong": true },
  "username": "Иванов И.И.",
  "org_name": "Университет",
  "clientLogotype": "https://...",
  "clientColor": "ff9900"
}
GET /users/check Публичный

Проверка учётных данных приглашённого пользователя (до регистрации). Для уже зарегистрированных учёток используйте POST /users/auth.

ПараметрТипОбязателенОписание
usernamestringДаЛогин/email приглашения
password / passwdstringДаПароль приглашения
GET /users/restore Публичный

Восстановление пароля — генерирует новый и отправляет на email.

ПараметрТипОбязателенОписание
emailstringДаEmail зарегистрированного пользователя
POST / GET /users/create Публичный

Завершение регистрации приглашённого пользователя. Поддерживаются POST и GET (query-параметры, как в legacy-клиентах). Самостоятельная заявка без готовой учётки — /users/register.

ПараметрТипОбязателенОписание
usernamestringДаЛогин приглашения
password / passwdstringДаПароль приглашения
emailstringДаEmail нового пользователя
fullnamestringДаФИО
usertypeintДаТип пользователя: 1 — студент, 2 — аспирант, 3 — преподаватель
user_passwordstringДаНовый пароль (мин. 6 символов)
POST / GET /users/register Публичный

Самостоятельная регистрация: создаётся пользователь с user_is_registered = false и заявка организации. Если почта подходит под домен автоодобрения, заявка подтверждается сразу. Иначе библиотекарям уходит уведомление и письмо. Если пароль не передан, он генерируется (8 символов).

ПараметрТипОбязателенОписание
organization_idintДаОрганизация, открытая для регистрации и с активной подпиской
role_idintДа4 преподаватель, 5 студент, 6 аспирант, 7 сотрудник
fullnamestringДаФИО
emailstringДаEmail и логин. Должен быть свободен
passwordstringНетМинимум 8 символов. Если пусто — генерируется
commentstringНетКомментарий к заявке
phonestringНетТелефон
date_of_birthdateНетДата рождения, формат Y-m-d

Успех:

{
  "success": true,
  "message": "Вы успешно зарегистрировались в системе",
  "data": { "user": { "email": "...", "fullname": "...", "role_id": 5 } }
}

Ошибка — конкретный текст в message, ошибки полей в errors:

{
  "success": false,
  "message": "Пользователь с таким email уже существует",
  "errors": { "email": ["Пользователь с таким email уже существует"] }
}

Другие тексты: Регистрация в выбранную организацию недоступна, Допустимые роли: студент, сотрудник, преподаватель или аспирант, Такой логин уже есть в системе, Не задана роль, Не найден запрос на регистрацию, Не удалось сохранить заявку на регистрацию, Некорректная дата рождения.

GET /users/code/check Публичный

Проверка кода приглашения до регистрации. Код ищется как {id}-{code}: числовой id записи и код из 5 цифр, например 15-48291. Ответ в формате версии API.

ПараметрТипОбязателенОписание
invite_codestringДаФормат ^\d+-\d{5}$

Успех: Код был успешно проверен.

Ошибки в message: Не указан код приглашения, Код приглашения некорректного формата, Код не найден, Срок жизни кода истек. Запросите новый в вашей библиотеке, Закончился лимит регистраций кода приглашений.Запросите новый в вашей библиотеке, У данного кода нет привязки к организации, У данного кода приглашения некорректная роль.

GET /users/code/info Публичный

Организация и роль кода приглашения. Проверки те же, что у /users/code/check. В каждом объекте только id и name, остальные поля не отдаются.

ПараметрТипОбязателенОписание
invite_codestringДаФормат ^\d+-\d{5}$

Успех:

{
  "success": true,
  "message": "Данные кода были успешно получены",
  "data": {
    "organization": { "id": 45, "name": "Организация" },
    "role": { "id": 5, "name": "Студент" }
  }
}

Ошибки в message те же, что у /users/code/check.

POST / GET /users/create/by-code Публичный

Регистрация по коду приглашения. Роль, организация и группа берутся из кода, пользователь сразу считается зарегистрированным. Поддерживаются POST и GET. Если пароль не передан, он генерируется (8 символов) и уходит на email.

ПараметрТипОбязателенОписание
invite_codestringДаКод в формате {id}-{5 цифр}
emailstringДаEmail и логин нового пользователя
fullnamestringДаФИО
passwordstringНетМинимум 8 символов. Если пусто — генерируется
commentstringНетКомментарий
phonestringНетТелефон
date_of_birthdateНетДата рождения, формат Y-m-d

После успеха registration_count кода увеличивается на 1 и пишется использование кода.

Успех: Вы успешно зарегистрировались в системе, в data.user — отправленные поля.

Ошибки в message: Код не найден, Срок жизни кода истек. Запросите новый в вашей библиотеке, Закончился лимит регистраций кода приглашений.Запросите новый в вашей библиотеке, Регистрация по этому коду недоступна, Код приглашения некорректного формата, Пользователь с таким email уже существует, Такой логин уже есть в системе, Не удалось зафиксировать использование кода приглашения, Некорректная дата рождения.

GET /oauth Публичный

OAuth-авторизация для внешних систем. Заголовок VERSION-API не требуется.

ПараметрТипОбязателенОписание
usernamestringДаЛогин/email
passwordstringДаПароль (сравнивается как md5(password) с БД)
timestringДаВременная метка
hashstringДаmd5(OAUTH_TOKEN + time)

Книги

Каталог и карточка публичные. Избранное защищённое. Канонический путь: /v3/books/...

GET /v3/books/catalog Публичный

Каталог книг организации (через Search API).

ПараметрТипОписание
pageintНомер страницы (по умолчанию 1)
ugnpstringКод УГНП (фильтр, 00 = все)
titlestringНазвание
authorsstringАвторы
pubhousestringИздательство
year_leftintГод от
year_rightintГод до
pub_typesstringТипы изданий через ||, напр. учебник||монография
publication_types_idsint[]ID типов изданий. Через запятую или publication_types_ids[]
authors_idsint[]ID авторов из справочника
purposes_idsint[]Тип образования, id из GET /v3/education-types (без вложенных levels). Алиас: education_type
education_level_idsint[]Уровни из ugnp_profile_levels: вложенные levels и пункты с level: true. Алиас: sub_purposes_ids
ugnps_idsint[]ID укрупнённых групп специальностей. Одиночный ugnp по-прежнему принимается
scientific_school_idintID научной школы
languages_idsint[]ID языков. Алиас одного значения: language_id
bbksstring[]Коды ББК, например 32.97
bbk_idsint[]ID узлов дерева ББК. Разворачиваются в коды листьев
isbnstringISBN, префикс
sort_bystringtitle — по названию, year — по году (новые сверху), id — по добавлению. Алиас: sort. Без параметра порядок прежний
selection_optionsintПодборка: 1–5 (см. раздел «Совместимость»). Алиас: selected_options

Элемент data.data[]: id, pagetitle, tv_year, tv_desc, tv_liability, tv_author

GET /v3/books/{id} Публичный

Карточка книги с оглавлением, рейтингом и статусом избранного.

ПараметрТипОписание
idint (path)ID книги

Дополнительные поля: isFavorite ("0"/"1"), size, content (оглавление), rating, user_rating

GET /v3/books/favorite Защищённый

Избранные книги пользователя.

ПараметрТипОписание
pageintСтраница
last_idintID последней записи (для инкрементальной подгрузки)
GET /v3/autocomplete Защищённый

Автодополнение для фильтров каталога.

ПараметрТипОписание
typeint13 — книги, 28 — журналы
fieldstringpagetitle, tv_author, tv_pubhouse
querystringСтрока поиска

Справочники фильтров книг

Списки для выпадающих фильтров каталога. Значения из ответов передаются в GET /v3/books/catalog параметрами из таблицы выше. Списки authors и scientific-schools постраничные: query, page, per_page.

GET /v3/authors Публичный

Авторы. Элемент: id, name. В каталог — authors_ids.

GET /v3/publication-types Публичный

Типы изданий по категориям: id, name, types[] (id, name). В каталог — publication_types_ids.

GET /v3/education-types Публичный

Типы образования из справочника purposes (категория «Образование»). Уровни — из ugnp_profile_levels, как GET /educational-levels/get на основном сайте: вложены в «Высшее образование», а одноимённые с типом не дублируются. Id уровня в каталог уходит как education_level_ids.

GET /v3/ugnps Публичный

Укрупнённые группы специальностей: id, name, code. В каталог — ugnps_ids.

GET /v3/scientific-schools Публичный

Научные школы: id, name. В каталог — scientific_school_id.

GET /v3/languages Публичный

Языки: id, name. В каталог — languages_ids.

GET /v3/bbks Публичный

Дерево ББК: id, name, code, children. В каталог можно передать bbk_ids выбранных узлов или готовые bbks (коды).

GET /v3/isbns Публичный

Подсказки ISBN для поля фильтра. Параметр query — от 3 символов, в ответе до 20 значений { "isbn": "..." }. В каталог выбранное значение уходит как isbn.

Журналы

GET /v3/journals/catalog Публичный
ПараметрТипОписание
pageintСтраница
titlestringНазвание
yearintГод выпуска. Равносильно year_left и year_right
year_left, year_rightintДиапазон года выпуска
pubhousestringНазвание издательства
publishers_idsint[]Издательства из GET /v3/journals/publishers
ugnpstringОдин УГНП (по умолчанию 00 — без фильтра)
ugnps_idsint[]Укрупнённые группы специальностей
vak0|1Входит ли в ВАК: 1 — да, 0 — нет. Без параметра — все
isbnstringISBN, поиск по началу

Элемент data.data[]: id, pagetitle, tv_year, tv_desc, tv_pubhouse, pubhouse, image

GET /v3/journals/{id} Публичный

Карточка журнала со списком доступных годов.

Поля: 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 отдаёт тот же справочник.

GET /v3/journals/years Публичный

Годы выпуска номеров, по убыванию. Элемент: id (год), name.

GET /v3/journals/publishers Публичный

Издательства журналов. Параметры: query, page, per_page. Элемент: id, name. В каталог передаётся publishers_ids.

GET /v3/journals/ugnps Публичный

Укрупнённые группы специальностей: id, name, code. В каталог — ugnps_ids.

GET /v3/journals/vak Публичный

Варианты «Входит ли в ВАК»: id=1 «Да», id=0 «Нет».

GET /v3/journals/isbns Публичный

Подсказки ISBN журналов. Параметр query от 3 символов, до 20 значений. Элемент: isbn.

GET /v3/journals/favorite Защищённый

Избранные номера журналов пользователя (как в старом API). Параметры: page, last_id

Номера журналов

GET /v3/years/{yearId} Публичный

Список номеров журнала за год.

Ответ: id, pagetitle, pagetitle_journal, numbers[]

GET /v3/numbers/{id} Публичный

Карточка номера журнала (аналогично книге: isFavorite, content, rating).

GET /v3/search Защищённый

Полнотекстовый поиск через Search API.

ПараметрТипОбязателенОписание
sstringДаПоисковый запрос
typeintНет*1 — книги, 2 — номера журналов. Обязателен, если не передан id
pageintНетСтраница результатов каталога (по умолчанию 1)
idintНетID издания: искать слово в оглавлении/страницах этой публикации

Главная

Блоки как на главной ipr-smart.local: новинки, популярные книги и журналы. Публичные. Параметр limit — размер списка (по умолчанию 10, максимум 50).

GET /v3/home/novelties Публичный

Новинки: публичные книги за последние 200 дней, язык RU (language_id = 1), по убыванию id.

Элемент как в каталоге книг: id, pagetitle, tv_author, image, …

GET /v3/home/popular Публичный

Популярные книги (is_popular = 1). Формат как у новинок.

GET /v3/home/journals Публичный

Популярные журналы (is_popular = 1), сортировка по последнему году выпуска. Элемент как в каталоге журналов плюс years_range (например 2019–2024).

GET /v3/last-reading Защищённый

Последнее чтение текущего пользователя (как 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.

PUT / GET /favorite/{id} Защищённый

Добавить в избранное.

ПараметрТипОписание
idint (path)ID публикации
typeint1 (книга) или 2 (журнал/номер). Если не передан — определяется по ID
DELETE /favorite/{id} Защищённый

Удалить из избранного. Параметр type — как выше.

Если издания не было в избранном: meta.message = «Издание не находилсь в избранном».

POST /favorite/uploads Защищённый

Пакетная синхронизация избранного. Поддерживаются два формата тела запроса.

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).

GET /v3/selections/get Защищённый

Список подборок текущего пользователя.

{
  "meta": { "success": true, "message": "Подборки были успешно получены" },
  "data": {
    "selections": [
      {
        "id": 1,
        "name": "К курсовой",
        "first_book_cover": "https://www.iprbookshop.ru/images/books/...",
        "books_count": 3
      }
    ]
  }
}
POST / GET /v3/selections/create Защищённый
ПараметрТипОписание
namestringНазвание подборки. Обязателен

Ответ: data.selection с полями id, name, first_book_cover, books_count.

GET /v3/selections/{id} Защищённый

Одна подборка пользователя. Чужие id → ошибка «Подборка не найдена».

POST / GET /v3/selections/{id}/update Защищённый

Переименовать. Параметр: name.

POST / GET / DELETE /v3/selections/{id}/delete Защищённый

Мягкое удаление подборки (deleted_at).

GET /v3/selections/{id}/books Защищённый
ПараметрТипОписание
pageintСтраница, по умолчанию 1
per_pageintРазмер страницы, по умолчанию 20 (макс. 50)

Ответ: data.books — пагинатор Laravel; элементы в формате каталога книг (pagetitle, tv_*, image).

POST / GET /v3/selections/{id}/books/add Защищённый

Пакетное добавление. Параметр: books_ids[] (или books_ids).

POST / GET /v3/selections/{id}/books/{bookId}/add Защищённый

Добавить одну книгу. Уже есть в подборке → «Данная книга уже была добавлена в эту подборку».

POST / GET /v3/selections/{id}/books/{bookId}/remove Защищённый

Удалить книгу из подборки.

Скачивание

Два защищённых метода без префикса /v3. Нужны uid, deviceId и ежедневный accessToken (или Authorization: Bearer …). Ответ — не JSON, а бинарный файл.

МетодШифрованиеДля кого
GET /downloadXOR (legacy)Текущие Android/Flutter клиенты с PdfDecryptStream
GET /ssl-downloadAES-256-CBC чанкамиНовые клиенты; тот же формат, что web-ридер IPR SMART
GET /download Защищённый

XOR-шифрованный PDF издания. Ключ шифрования — первые 16 символов deviceToken устройства (токен из ответа авторизации, поле устройства access_token). Ежедневный параметр accessToken к шифрованию не относится.

ПараметрТипОписание
idintID издания. Обязателен
typestringГде искать файл. 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.

GET /ssl-download Защищённый

Те же query-параметры и авторизация, что у /download. Шифрование — AES-256-CBC по чанкам, как в web-ридере ipr-smart.local.

ПараметрТипОписание
idintID издания. Обязателен
typestringКак у /download
uid, deviceId, accessToken, date—Стандартная авторизация API

Что отличается от /download

/download/ssl-download
АлгоритмXOR с deviceTokenAES-256-CBC, PKCS7
КлючПервые 16 символов deviceToken (известен клиенту заранее)Случайные 32 байта на каждый запрос
Где ключЛокально после authЗаголовок ответа X-Session-Key
Длина тела= размеру PDFБольше PDF (заголовки кадров + padding)
Content-LengthЕсть (= размер файла)Нет — не опирайтесь на него

Обязательный порядок на клиенте

  1. Сделать GET /ssl-download?... с теми же auth-параметрами, что для остальных защищённых методов. Заголовок VERSION-API: 3 обязателен.
  2. Проверить HTTP-статус: 200 — ок; 404 с пустым телом — ошибка (нет файла / нет доступа / нет устройства). JSON-ошибки не будет.
  3. Сразу прочитать заголовки ответа (до или вместе с телом — ключ в теле не дублируется):
    ЗаголовокОбязателен клиентуОписание
    X-Session-KeyДаBase64 → ровно 32 байта AES-ключа
    X-Encryption-ModeРекомендуетсяДолжен быть chunked
    X-Chunk-SizeНетРазмер plaintext-чанка на сервере (обычно 4194304). На расшифровку не влияет
    X-IVНетБазовый IV сессии. Для расшифровки не нужен: IV каждого чанка уже лежит в теле
  4. Скачать тело как сырые байты (не UTF-8 string, не JSON).
  5. Разобрать тело на кадры и расшифровать каждый AES-256-CBC.
  6. Склеить plaintext-чанки по порядку → это PDF. Сохранить/открыть.

Формат тела (кадр за кадром)

offset 0:
  uint8   ivLength          // всегда 16
  bytes   chunkIv[ivLength] // 16 байт
  uint32  cipherLength      // big-endian (network byte order)
  bytes   ciphertext[cipherLength]

далее — следующий кадр, пока не кончатся байты
После последнего кадра лишних байт быть не должно. Если остаток > 0 — файл обрезан или повреждён, расшифровку считать ошибкой.

Алгоритм расшифровки одного кадра

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-"
}

Важные нюансы для мобильного клиента

Ошибки

СитуацияЧто видит клиент
Нет 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 публикации (книги).

GET /v3/{id}/quotes/get Защищённый

Список цитат. Элемент: ext_id, page, text, create_date

GET /v3/{id}/quotes/add Защищённый
ПараметрОписание
page_idНомер страницы
textТекст цитаты
GET /v3/{id}/quotes/delete Защищённый

Параметр: quote_id

GET /v3/quotes/{id}/update Защищённый

Обновить цитату. Параметр: text

GET /v3/{id}/bookmarks/get Защищённый

Список закладок (формат аналогичен цитатам).

GET /v3/{id}/bookmarks/add Защищённый

Добавить закладку.

ПараметрОписание
page_idНомер страницы (обязателен)
textКомментарий (в legacy API; в новой схеме не сохраняется)
GET /v3/{id}/bookmarks/delete Защищённый

Параметр: bookmark_id

GET /v3/{id}/content/download Защищённый

Оглавление публикации. Ответ: [{ "page": 1, "text": "Глава 1" }, ...]

GET /v3/{id}/content/search Защищённый

Поиск слова в оглавлении/тексте страниц издания.

ПараметрОписание
sИскомое слово или фраза
pageОпционально: искать только на этой странице

Ответ: [{ "page": 15, "text": "..." }, ...]

GET /v3/{id}/rating/update Защищённый

Установить оценку. Параметр: 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: нет файла, неверный токен или нет idHTTP 404, пустое тело
Несовместимая collation MySQLУстановите DB_COLLATION=utf8mb4_unicode_ci в .env

IPR SMART Mobile API · Laravel 11.57.1 · Совместимость с old-mobible-api