Add docker

This commit is contained in:
Evgenii Saenko
2025-12-17 11:52:18 +03:00
parent 2d5b329b36
commit ea390b1533
38 changed files with 1359 additions and 165 deletions

102
docs/architecture.md Normal file
View File

@@ -0,0 +1,102 @@
# Архитектура и структура сервиса
Документ описывает как организован сервер дипломного проекта: какие слои присутствуют, как они взаимодействуют и где искать ключевые модули.
## Общий обзор
Сервис написан на Kotlin с использованием Ktor и разворачивается как Netty-приложение (`Application.kt`). Приложение поднимает инфраструктурные плагины (HTTP, сериализация, безопасность) и регистрирует доменные маршруты. Все функции сгруппированы в повторяющийся паттерн `Controller → Service → Repository → Entity`, что делает поддержку CRUD-операций симметричной во всех доменных областях.
Высокоуровневый поток запросов:
1. HTTP-вызов попадает в `Routing.kt`, где маршруты разделены на публичную и админскую зоны (`/api/v1/...`).
2. Контроллер модуля занимается десериализацией запроса, базовой валидацией и формированием ответа.
3. Сервис инкапсулирует бизнес-правила: публикация материалов, фильтрация, проверка прав, подготовка DTO.
4. Репозиторий обращается к PostgreSQL через Ktorm, используя HikariCP-пул соединений.
5. Исключения (`ValidationException`, `NotFoundException`) перехватывает `StatusPages`, что обеспечивает единый контракт ошибок.
Графическая схема слоев расположена в `docs/crud-model.svg`.
## Структура каталогов
```
src/main/kotlin
├── app # Bootstrap: Application, Routing, Security, HTTP, Database
├── shared # Переиспользуемые DTO, пагинация, ошибки
└── modules # Доменные модули (admin, news, service, serviceCategory, lead)
src/main/resources
├── application.yaml # Конфигурация Ktor
└── openapi # Спецификации API
src/test/kotlin # Зеркало production-пакетов для тестов
docs/
├── crud-model.svg # Диаграмма последовательности CRUD
└── architecture.md # Текущий документ
```
## Инфраструктурный слой (`app/*`)
- **Application.module** точка входа, выстраивает последовательность `configureDatabase → configureHTTP → configureSerialization → configureSecurity → configureRouting`.
- **Database.kt** инициализация пула HikariCP (настройки берутся из `.env`), регистрация `Database` в `Application.attributes`.
- **Http.kt** подключает плагины Ktor (CORS, Compression, CachingHeaders, StatusPages, Swagger UI). Здесь же централизованно обрабатываются исключения.
- **Serialization.kt** `ContentNegotiation` с `kotlinx.serialization`.
- **Security.kt** загрузка параметров JWT из окружения, настройка схемы `admin-auth`, валидация `JWTPrincipal`.
- **Routing.kt** собирает зависимости модулей, подключает публичные маршруты и оборачивает админские роуты в `authenticate("admin-auth")`.
## Доменные модули (`modules/*`)
Каждый модуль состоит из четырех файлов:
- `Controller.kt` набор Ktor-маршрутов для публичной и/или админской зоны.
- `Service.kt` бизнес-логика. Например, `NewsService` отвечает за публикацию, пагинацию и преобразование сущностей в DTO, а `ServiceService` поддерживает фильтры по статусу, категории и ценовому диапазону.
- `Repository.kt` слой доступа к данным на Ktorm: построение SQL, пагинация, CRUD-операции, трансформации в `Entity`.
- `Entity.kt` / DTO описания таблиц, модели для сериализации и транспортировки данных.
### Admin
Отвечает за регистрацию, авторизацию и управление администраторами. Использует `BcryptPasswordHasher` для хранения паролей и `TokenService` для выпуска JWT. Публичная часть (`/admin/login`) возвращает токен; защищенная (`/admin/*`) требует `admin-auth`.
### News
Два набора маршрутов: публичные (`/news`, `/news/{slug}`) и админские (`/admin/news`). Сервис применяет правила публикации и формирует страницы (`Page<T>`). Репозиторий обрабатывает SQL для списков и выборок по `slug`.
### Services и Service Categories
`ServiceController` поддерживает сложные фильтры (категория, диапазон цен, статус). `ServiceService` связывает услуги с категориями, обеспечивая получение вложенных DTO. `ServiceCategory*` модули обслуживают справочник категорий и проверяют уникальность `slug`.
### Leads
`publicLeadRoutes` принимает заявки с сайта (`POST /leads`), `adminLeadRoutes` предоставляет CRUD для операторов. Сервис выполняет базовую валидацию и поиск по строке `q`.
## Общие компоненты (`shared/*`)
- `shared.pagination.Page` универсальный ответ для списков.
- `shared.errors.ValidationException/NotFoundException` типы, которые перехватываются `StatusPages` и автоматически конвертируются в HTTP-ответ.
## Работа с БД
- **База** PostgreSQL, подключение через Ktorm + HikariCP (`jdbc:postgresql://...`).
- **Пулы** настраиваются через переменные окружения (`DB_HOST`, `DB_POOL_MAX` и т.д.).
- **Репозитории** реализованы вручную на SQL Builder, что дает контроль над запросами и связями между таблицами (`services``service_categories`, `news`, `leads`, `admins`).
## Безопасность
- Единственная схемa аутентификации JWT `admin-auth`.
- Конфигурация (`JWT_SECRET`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_EXPIRES_MIN`) читается из `.env` или переменных окружения.
- Public маршруты доступны без токена, все `admin*Routes` подключены внутри `authenticate`.
## Сборка и эксплуатация
- Основные команды Gradle:
- `./gradlew run` локальный сервер.
- `./gradlew test` прогон тестов (обязателен перед пушем).
- `./gradlew buildFatJar` сборка standalone JAR.
- `./gradlew buildImage` / `runDocker` сборка и запуск docker-образа.
- Конфигурация HTTP-порта, логирования и OpenAPI лежит в `src/main/resources/application.yaml` и `resources/openapi`.
- Для обновления документации добавляйте артефакты в `docs/` (например, текущую архитектурную схему и диаграмму CRUD).
## Тестирование
Тесты зеркалируют production-пакеты (`src/test/kotlin/...`). При добавлении новой функциональности рекомендуется:
1. Класть тесты в соответствующий пакет модуля (например, `modules/news/NewsServiceTest.kt`).
2. Проверять ветки успеха, валидации и авторизации.
3. Запускать `./gradlew test` перед коммитом, чтобы убедиться в корректной работе всего набора.

147
docs/crud-model.svg Normal file
View File

@@ -0,0 +1,147 @@
<svg width="1200" height="950" viewBox="0 0 1200 950" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
.bg { fill: #f8fafc; }
.card { fill: #ffffff; stroke: #0f172a; stroke-width: 1.5; }
.title { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 32px; font-weight: 600; fill: #0f172a; }
.subtitle { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 16px; fill: #475569; }
.card-title { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 17px; font-weight: 600; fill: #0f172a; }
.card-body { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 13px; fill: #334155; }
.note { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 13px; fill: #475569; }
.arrow { stroke: #0f172a; stroke-width: 1.8; fill: none; }
.dashed { stroke-dasharray: 6 6; }
</style>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0f172a" />
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="950" rx="32" />
<text x="600" y="45" text-anchor="middle" class="title">CRUD-модель сервера Diploma</text>
<text x="600" y="75" text-anchor="middle" class="subtitle">Ktor обрабатывает HTTP-запросы через Controllers, Services, Repositories и таблицы PostgreSQL.</text>
<!-- Client surfaces -->
<rect class="card" x="150" y="90" width="220" height="100" rx="14" />
<text x="260" y="120" text-anchor="middle" class="card-title">Публичные каналы</text>
<text x="260" y="150" text-anchor="middle" class="card-body">Маркетинговый сайт</text>
<text x="260" y="170" text-anchor="middle" class="card-body">Лендинги и формы</text>
<rect class="card" x="830" y="90" width="220" height="100" rx="14" />
<text x="940" y="120" text-anchor="middle" class="card-title">Админ-консоль</text>
<text x="940" y="150" text-anchor="middle" class="card-body">Внутренний кабинет</text>
<text x="940" y="170" text-anchor="middle" class="card-body">CMS, отчеты</text>
<!-- Ktor layer -->
<rect class="card" x="420" y="200" width="360" height="140" rx="18" />
<text x="600" y="235" text-anchor="middle" class="card-title">Ktor HTTP-слой</text>
<text x="600" y="260" text-anchor="middle" class="card-body">Routing.kt · Serialization.kt · Security.kt</text>
<text x="600" y="285" text-anchor="middle" class="card-body">Проверка JWT, JSON-парсинг,</text>
<text x="600" y="305" text-anchor="middle" class="card-body">маршрутизация по модулям</text>
<!-- Shared utilities -->
<rect class="card" x="1000" y="230" width="150" height="120" rx="14" />
<text x="1075" y="260" text-anchor="middle" class="card-title">shared/*</text>
<text x="1075" y="285" text-anchor="middle" class="card-body">Пагинация</text>
<text x="1075" y="305" text-anchor="middle" class="card-body">Ошибки и DTO</text>
<!-- Controllers row -->
<rect class="card" x="80" y="360" width="220" height="150" rx="16" />
<text x="190" y="390" text-anchor="middle" class="card-title">NewsController</text>
<text x="190" y="415" text-anchor="middle" class="card-body">GET /news и /news/{slug}</text>
<text x="190" y="435" text-anchor="middle" class="card-body">Админ: список и создание</text>
<text x="190" y="455" text-anchor="middle" class="card-body">обновление, публикация, удаление</text>
<rect class="card" x="340" y="360" width="220" height="150" rx="16" />
<text x="450" y="390" text-anchor="middle" class="card-title">ServiceController</text>
<text x="450" y="415" text-anchor="middle" class="card-body">Публичный список/просмотр</text>
<text x="450" y="435" text-anchor="middle" class="card-body">Фильтры: категория, цена</text>
<text x="450" y="455" text-anchor="middle" class="card-body">Админ: CRUD и статус</text>
<rect class="card" x="600" y="360" width="220" height="150" rx="16" />
<text x="710" y="390" text-anchor="middle" class="card-title">ServiceCategoryController</text>
<text x="710" y="415" text-anchor="middle" class="card-body">Публичный список и slug</text>
<text x="710" y="435" text-anchor="middle" class="card-body">Админ: список, создание</text>
<text x="710" y="455" text-anchor="middle" class="card-body">обновление, удаление</text>
<rect class="card" x="860" y="360" width="220" height="150" rx="16" />
<text x="970" y="390" text-anchor="middle" class="card-title">LeadController</text>
<text x="970" y="415" text-anchor="middle" class="card-body">POST /leads с сайта</text>
<text x="970" y="435" text-anchor="middle" class="card-body">Админ: список и чтение</text>
<text x="970" y="455" text-anchor="middle" class="card-body">Удаление заявок</text>
<!-- Services row -->
<rect class="card" x="80" y="530" width="220" height="120" rx="16" />
<text x="190" y="560" text-anchor="middle" class="card-title">NewsService</text>
<text x="190" y="585" text-anchor="middle" class="card-body">Правила публикации, DTO</text>
<text x="190" y="605" text-anchor="middle" class="card-body">Страницы и статусы</text>
<rect class="card" x="340" y="530" width="220" height="120" rx="16" />
<text x="450" y="560" text-anchor="middle" class="card-title">ServiceService</text>
<text x="450" y="585" text-anchor="middle" class="card-body">Фильтры по статусу/категории</text>
<text x="450" y="605" text-anchor="middle" class="card-body">Контроль цены, связи</text>
<rect class="card" x="600" y="530" width="220" height="120" rx="16" />
<text x="710" y="560" text-anchor="middle" class="card-title">ServiceCategoryService</text>
<text x="710" y="585" text-anchor="middle" class="card-body">Таксономия и slug</text>
<text x="710" y="605" text-anchor="middle" class="card-body">CRUD категорий</text>
<rect class="card" x="860" y="530" width="220" height="120" rx="16" />
<text x="970" y="560" text-anchor="middle" class="card-title">LeadService</text>
<text x="970" y="585" text-anchor="middle" class="card-body">Валидация контактов</text>
<text x="970" y="605" text-anchor="middle" class="card-body">Поиск и пагинация</text>
<!-- Repository row -->
<rect class="card" x="80" y="680" width="220" height="120" rx="16" />
<text x="190" y="710" text-anchor="middle" class="card-title">NewsRepository</text>
<text x="190" y="735" text-anchor="middle" class="card-body">SQL-запросы для news</text>
<text x="190" y="755" text-anchor="middle" class="card-body">Админ и публичные выборки</text>
<rect class="card" x="340" y="680" width="220" height="120" rx="16" />
<text x="450" y="710" text-anchor="middle" class="card-title">ServiceRepository</text>
<text x="450" y="735" text-anchor="middle" class="card-body">Джоины услуг и категорий</text>
<text x="450" y="755" text-anchor="middle" class="card-body">Фильтры по цене</text>
<rect class="card" x="600" y="680" width="220" height="120" rx="16" />
<text x="710" y="710" text-anchor="middle" class="card-title">ServiceCategoryRepository</text>
<text x="710" y="735" text-anchor="middle" class="card-body">Метаданные категорий</text>
<text x="710" y="755" text-anchor="middle" class="card-body">Хранение slug</text>
<rect class="card" x="860" y="680" width="220" height="120" rx="16" />
<text x="970" y="710" text-anchor="middle" class="card-title">LeadRepository</text>
<text x="970" y="735" text-anchor="middle" class="card-body">Прием заявок</text>
<text x="970" y="755" text-anchor="middle" class="card-body">Админские запросы</text>
<!-- Database layer -->
<rect class="card" x="100" y="830" width="1000" height="110" rx="20" />
<text x="600" y="860" text-anchor="middle" class="card-title">Схема PostgreSQL</text>
<text x="600" y="885" text-anchor="middle" class="card-body">news, services, service_categories, leads</text>
<text x="600" y="905" text-anchor="middle" class="card-body">Временные метки, статусы, связи</text>
<!-- Arrows -->
<path class="arrow" marker-end="url(#arrow)" d="M 260 190 L 600 200" />
<path class="arrow" marker-end="url(#arrow)" d="M 940 190 L 600 200" />
<path class="arrow" marker-end="url(#arrow)" d="M 600 340 L 190 360" />
<path class="arrow" marker-end="url(#arrow)" d="M 600 340 L 450 360" />
<path class="arrow" marker-end="url(#arrow)" d="M 600 340 L 710 360" />
<path class="arrow" marker-end="url(#arrow)" d="M 600 340 L 970 360" />
<!-- Vertical flows -->
<path class="arrow" marker-end="url(#arrow)" d="M 190 510 L 190 530" />
<path class="arrow" marker-end="url(#arrow)" d="M 450 510 L 450 530" />
<path class="arrow" marker-end="url(#arrow)" d="M 710 510 L 710 530" />
<path class="arrow" marker-end="url(#arrow)" d="M 970 510 L 970 530" />
<path class="arrow" marker-end="url(#arrow)" d="M 190 650 L 190 680" />
<path class="arrow" marker-end="url(#arrow)" d="M 450 650 L 450 680" />
<path class="arrow" marker-end="url(#arrow)" d="M 710 650 L 710 680" />
<path class="arrow" marker-end="url(#arrow)" d="M 970 650 L 970 680" />
<path class="arrow" marker-end="url(#arrow)" d="M 190 800 L 190 830" />
<path class="arrow" marker-end="url(#arrow)" d="M 450 800 L 450 830" />
<path class="arrow" marker-end="url(#arrow)" d="M 710 800 L 710 830" />
<path class="arrow" marker-end="url(#arrow)" d="M 970 800 L 970 830" />
<text x="600" y="925" text-anchor="middle" class="note">Каждый модуль следует паттерну Controller → Service → Repository и покрывает CRUD.</text>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

119
docs/db-structure.md Normal file
View File

@@ -0,0 +1,119 @@
# Структура БД и модель данных
Документ описывает схемы PostgreSQL, которые используются приложением, а также слой абстракции на базе Ktorm ORM.
## Технологический стек
- **PostgreSQL** основная СУБД. Подключение выполняется через HikariCP (см. `app/Database.kt`), параметры берутся из переменных окружения `DB_*`.
- **Ktorm ORM** легковесная ORM, которая:
- предоставляет интерфейсы `Entity` для объектного представления строк;
- описывает таблицы через `object Table<T>` (например, `object NewsT : Table<News>("t_news")`);
- дает `Database.sequenceOf(Table)` для CRUD-операций и построения SQL через DSL.
Каждая доменная сущность имеет:
1. `interface <Entity> : Entity<<Entity>>` декларация полей.
2. `object <Table> : Table<<Entity>>("table_name")` описание колонок/ключей и связей через `bindTo`/`references`.
3. DTO для сериализации в API.
## Таблицы
### `t_admins`
| Колонка | Тип | Описание |
|----------------|------------------|---------------------------------------|
| `id` | `bigint` (PK) | Идентификатор администратора |
| `username` | `varchar` | Уникальное имя пользователя |
| `password_hash`| `varchar` | Хеш пароля (bcrypt) |
| `created_at` | `timestamp` | Дата регистрации |
| `last_login_at`| `timestamp` nul. | Время последней авторизации |
Сущность: `AdminEntity`, таблица: `AdminUsers`. Используется модулем `admin` для регистрации, логина, смены паролей и удаления аккаунтов.
### `t_news`
| Колонка | Тип | Описание |
|----------------|------------------|--------------------------------------------------|
| `id` | `bigint` (PK) | Идентификатор новости |
| `title` | `varchar` | Заголовок |
| `slug` | `varchar` | Уникальный slug для ссылок |
| `summary` | `varchar` | Краткое описание |
| `content` | `text` | Основной текст |
| `status` | `varchar` | `DRAFT` \| `PUBLISHED` \| `ARCHIVED` |
| `published_at` | `timestamp` nul. | Дата публикации |
| `image_url` | `varchar` nul. | Ссылка на изображение |
| `created_at` | `timestamp` | Дата создания |
| `updated_at` | `timestamp` | Дата последнего обновления |
Сущность: `News`, таблица: `NewsT`. Репозиторий использует фильтры по статусу и slug для публичных и админских запросов.
### `t_service_categories`
| Колонка | Тип | Описание |
|---------|---------------|-------------------------|
| `id` | `bigint` (PK) | Идентификатор категории |
| `name` | `varchar` | Название |
| `slug` | `varchar` | Уникальный slug |
Сущность: `ServiceCategoryEntity`, таблица: `ServiceCategories`. Используется как справочник категорий услуг.
### `t_services`
| Колонка | Тип | Описание |
|--------------|-------------------|----------------------------------------------------|
| `id` | `bigint` (PK) | Идентификатор услуги |
| `title` | `varchar` | Название |
| `slug` | `varchar` | Уникальный slug |
| `description`| `text` | Подробное описание |
| `price_from` | `decimal` nul. | Нижняя граница стоимости |
| `image_url` | `varchar` nul. | Изображение |
| `status` | `varchar` | `PUBLISHED` \| `DRAFT` \| `ARCHIVED` |
| `category_id`| `bigint` FK | Ссылка на `t_service_categories.id` (может быть `NULL`) |
| `created_at` | `timestamp` | Дата создания |
| `updated_at` | `timestamp` | Дата обновления |
Сущность: `ServiceEntity`, таблица: `Services`. Поле `category` смоделировано через `references(ServiceCategories)` и возвращает `ServiceCategoryEntity?`. Сервис объединяет услуги и категории для публичного и админского API.
### `t_users` (лиды)
| Колонка | Тип | Описание |
|-------------|-----------------|------------------------------------------|
| `id` | `bigint` (PK) | Идентификатор лида |
| `full_name` | `varchar` | Имя и фамилия |
| `email` | `varchar` | Контактный email |
| `phone` | `varchar` nul. | Телефон |
| `created_at`| `timestamp` | Дата поступления заявки |
Сущность: `LeadEntity`, таблица: `Leads`. Несмотря на название таблицы `t_users`, фактически хранит только заявки. Админский модуль использует пагинацию и поиск по `full_name`/`email`.
## Связи между сущностями
- **Service → ServiceCategory (многие к одному)**: `Services.category_id` ссылается на `ServiceCategories.id`. Ktorm позволяет навигировать через `ServiceEntity.category`. При выборе услуг можно жадно загружать категорию и формировать вложенный DTO.
- **Admin, News, Leads** независимые сущности без внешних ключей на другие таблицы (в текущей версии).
- **Status/enum поля** находятся в бизнес-логике: нет отдельных таблиц для статусов, их значения валидируются сервисами.
## Использование Ktorm
- **Entity интерфейсы** объявляют свойства и их типы. Экземпляры создаются через `Entity.Factory`.
- **Table объекты** задают имя таблицы, колонки и связи. Пример:
```kotlin
object Services : Table<ServiceEntity>("t_services") {
val title = varchar("title").bindTo { it.title }
val category = long("category_id").references(ServiceCategories) { it.category }
}
```
- **Расширения Database** в каждом модуле есть свой `val Database.<entities>` (например, `Database.news`) для получения `sequenceOf(Table)`:
```kotlin
val Database.news get() = this.sequenceOf(NewsT)
```
Это скрывает детали доступа к данным и дает типобезопасные операции (`filter`, `sortedBy`, `take`, `drop`, `insert`, `update` и т.д.).
- **DTO слой** отделяет Ktorm-entity от сериализуемых объектов (например, `NewsDTO`, `ServiceDTO`). Преобразование выполняется в сервисах.
## Итог
База данных состоит из пяти основных таблиц, объединенных единым пулом соединений и общими практиками (таймстемпы, статусы, slug). Слабое связывание между сущностями делает схему гибкой, а Ktorm обеспечивает лаконичный и типобезопасный доступ к данным без тяжелых ORM-схем. Диаграмма CRUD-слоя (`docs/crud-model.svg`) дополняет данное описание визуальным представлением потока данных.

View File

@@ -0,0 +1,69 @@
<svg width="1200" height="800" viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
.bg { fill: #ffffff; }
.title { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 26px; font-weight: 600; fill: #1f2937; }
.node { fill: #f8fafc; stroke: #1f2937; stroke-width: 2; rx: 16; }
.node-title { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 20px; font-weight: 600; fill: #111827; }
.node-body { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 15px; fill: #374151; }
.arrow { stroke: #1f2937; stroke-width: 2; fill: none; marker-end: url(#arrowhead); }
.arrow-label { font-family: 'Inter', 'Segoe UI', sans-serif; font-size: 15px; fill: #1f2937; }
.db { fill: #f8fafc; stroke: #1f2937; stroke-width: 2; }
</style>
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#1f2937" />
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="800" />
<text x="600" y="60" text-anchor="middle" class="title">Общая схема взаимодействия: Клиент → Ktor Server → PostgreSQL</text>
<!-- Client node -->
<rect class="node" x="20" y="180" width="300" height="420" />
<text x="180" y="220" text-anchor="middle" class="node-title">Клиент (React SPA)</text>
<text x="60" y="270" class="node-body">• UI Components</text>
<text x="60" y="305" class="node-body">• MobX Stores</text>
<text x="60" y="340" class="node-body">• fetch API</text>
<text x="60" y="375" class="node-body">• Браузерное приложение</text>
<!-- Server node -->
<rect class="node" x="460" y="140" width="320" height="500" />
<text x="620" y="180" text-anchor="middle" class="node-title">Ktor Server (Kotlin)</text>
<text x="490" y="230" class="node-body">• Routing</text>
<text x="490" y="265" class="node-body">• Controllers</text>
<text x="490" y="300" class="node-body">• Services</text>
<text x="490" y="335" class="node-body">• Repositories</text>
<text x="490" y="370" class="node-body">• Ktorm ORM</text>
<text x="490" y="405" class="node-body">• JWT Authentication</text>
<text x="490" y="440" class="node-body">• Запуск на Netty runtime</text>
<!-- Database cylinder -->
<g transform="translate(900,200)">
<ellipse class="db" cx="155" cy="0" rx="140" ry="25" />
<rect class="db" x="15" y="0" width="280" height="360" ry="0" />
<ellipse class="db" cx="155" cy="360" rx="140" ry="25" />
<text x="155" y="40" text-anchor="middle" class="node-title">База данных PostgreSQL</text>
<text x="50" y="90" class="node-body">• t_admins</text>
<text x="50" y="125" class="node-body">• t_news</text>
<text x="50" y="160" class="node-body">• t_services</text>
<text x="50" y="195" class="node-body">• t_service_categories</text>
<text x="50" y="230" class="node-body">• t_users (leads)</text>
<text x="50" y="265" class="node-body">• Транзакционность (ACID)</text>
</g>
<!-- Arrows -->
<path class="arrow" d="M 320 320 L 460 320" />
<text x="390" y="300" text-anchor="middle" class="arrow-label">REST API /api/v1/*</text>
<path class="arrow" d="M 780 320 L 915 320" />
<text x="850" y="290" text-anchor="middle" class="arrow-label">
<tspan>SQL-запросы</tspan>
<tspan x="845" dy="1.2em">через Ktorm</tspan>
</text>
<path class="arrow" d="M 915 400 L 780 400" />
<text x="850" y="385" text-anchor="middle" class="arrow-label">JSON-ответы</text>
<path class="arrow" d="M 460 400 L 320 400" />
<text x="395" y="385" text-anchor="middle" class="arrow-label">JSON-ответы</text>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

311
docs/rest-api.md Normal file
View File

@@ -0,0 +1,311 @@
# REST API (Swagger-Style)
## Общая информация
- **Базовый URL**: `http://0.0.0.0:8080/api/v1`
- **Форматы**: все конечные точки принимают и возвращают `application/json`, если не указан иной `Content-Type`.
- **Часовой формат**: даты/время сериализуются в `ISO_LOCAL_DATE_TIME`, например `2024-05-12T13:45:00`.
- **JWT-аутентификация**: приватные маршруты располагаются под `/api/v1/admin/**` и требуют заголовок `Authorization: Bearer <token>` из `POST /admin/login`.
- **Ошибки**: при валидационных/авторизационных ошибках сервер возвращает код `4xx` с телом `{"error": "описание"}`.
## Базовые схемы
| Схема | Описание |
| --- | --- |
| **Page<T>** | Обёртка пагинации: `items` (список сущностей `T`), `total` (общее количество), `limit` (число элементов на странице), `offset` (смещение). |
| **ServiceCategoryDTO** | `{ id: number, name: string, slug: string }`. |
| **ServiceDTO** | `{ id, title, slug, description, priceFrom: number\|null, imageUrl: string\|null, status: "PUBLISHED"\|"DRAFT"\|"ARCHIVED", category: ServiceCategoryDTO\|null, createdAt, updatedAt }`. |
| **NewsDTO** | `{ id, title, slug, summary, content, status: "draft"\|"published"\|"archived", imageUrl: string\|null, publishedAt: string\|null }`. |
| **LeadDTO** | `{ id, fullName, email, phone: string\|null, createdAt }`. |
| **AdminDTO** | `{ id, username, createdAt }`. |
---
## Администраторы и аутентификация
| Метод | Путь | Требуется JWT | Описание |
| --- | --- | --- | --- |
| POST | `/admin/login` | Нет | Получение JWT токена. |
| GET | `/admin/password_hash` | Нет | Вспомогательный эндпойнт для генерации bcrypt-хэша. |
| GET | `/admin` | Да | Получить профиль текущего администратора. |
| POST | `/admin` | Да | Создать нового администратора. |
| PUT | `/admin/{id}/password` | Да | Сменить пароль администратора. |
| DELETE | `/admin/{id}` | Да | Удалить администратора. |
### POST /api/v1/admin/login
**Тело запроса**
| Поле | Тип | Обязательно | Примечание |
| --- | --- | --- | --- |
| `username` | string | да | Мин. 3 символа (`[A-Za-z0-9_.-]`). |
| `password` | string | да | Мин. 8 символов. |
**Ответ 200**
| Поле | Тип | Примечание |
| --- | --- | --- |
| `id` | number | Идентификатор администратора. |
| `username` | string | Введённое имя. |
| `token` | string | JWT access token. |
| `tokenType` | string | Всегда `Bearer`. |
| `expiresInMinutes` | number | Время жизни токена (минуты). |
Пример:
```json
{
"id": 1,
"username": "admin",
"token": "eyJhbGciOi...",
"tokenType": "Bearer",
"expiresInMinutes": 60
}
```
### GET /api/v1/admin/password_hash
**Query-параметры**
| Параметр | Тип | Обязательно | Описание |
| --- | --- | --- | --- |
| `password` | string | нет | Исходный пароль. По умолчанию `admin123`. |
**Ответ 200**: `{ "pass": "<bcrypt-hash>" }`.
### GET /api/v1/admin
Возвращает `AdminDTO` текущего пользователя по subject токена.
### POST /api/v1/admin
**Тело запроса** `AdminRegisterRequest`:
| Поле | Тип | Обязательно | Примечание |
| --- | --- | --- | --- |
| `username` | string | да | Уникальное имя (регулярное выражение как при логине). |
| `password` | string | да | Мин. 8 символов. |
**Ответ 201** `AdminRegisterResponse`:
| Поле | Тип |
| --- | --- |
| `id` | number |
| `username` | string |
### PUT /api/v1/admin/{id}/password
**Параметры пути**: `id` numeric ID.
**Тело запроса** `ChangePasswordRequest`:
| Поле | Тип | Обязательно | Примечание |
| --- | --- | --- | --- |
| `currentPassword` | string | да | Текущий пароль (проверяется через bcrypt). |
| `newPassword` | string | да | Новый пароль, минимум 8 символов. |
**Ответ 200**: `{ "updated": true }`.
### DELETE /api/v1/admin/{id}
Удаляет администратора. Ответ `200 OK`: `{ "deleted": true }`.
---
## Лиды (заявки)
| Метод | Путь | JWT | Описание |
| --- | --- | --- | --- |
| POST | `/leads` | Нет | Оставить заявку. |
| GET | `/admin/leads` | Да | Список лидов с фильтром. |
| GET | `/admin/leads/{id}` | Да | Получить лид по ID. |
| DELETE | `/admin/leads/{id}` | Да | Удалить лид. |
### POST /api/v1/leads
**Тело запроса** `LeadCreateRequest`:
| Поле | Тип | Обязательно | Примечание |
| --- | --- | --- | --- |
| `fullName` | string | да | Имя клиента. |
| `email` | string | да | Проверяется регуляркой `^[\\w.+-]+@[\\w.-]+\\.[A-Za-z]{2,}$`. |
| `phone` | string | нет | Любой формат, сохраняется как строка. |
**Ответ 201**: `{ "id": 42 }`.
### GET /api/v1/admin/leads
**Query-параметры**
| Имя | Тип | По умолчанию | Описание |
| --- | --- | --- | --- |
| `limit` | integer | 50 | Размер страницы. |
| `page` | integer | 1 | Должна быть ≥ 1, иначе `400`. |
| `q` | string | null | Поиск по `fullName`, `email`, `phone`. |
**Ответ 200**: `Page<LeadDTO>`.
Пример элемента: `{ "id": 7, "fullName": "Иван Иванов", "email": "ivan@example.com", "phone": "+79001234567", "createdAt": "2024-05-05T10:34:00" }`.
### GET /api/v1/admin/leads/{id}
Параметр `id` (long). Ответ `LeadDTO`.
### DELETE /api/v1/admin/leads/{id}
Ответ `{ "deleted": true }`.
---
## Новости
| Метод | Путь | JWT | Описание |
| --- | --- | --- | --- |
| GET | `/news` | Нет | Пагинированный список опубликованных новостей. |
| GET | `/news/{slug}` | Нет | Получение новости по `slug`. |
| GET | `/admin/news` | Да | Список всех новостей. |
| POST | `/admin/news` | Да | Создать новость. |
| PUT | `/admin/news/{slug}` | Да | Обновить новость. |
| POST | `/admin/news/{slug}/publish` | Да | Публикация новости. |
| DELETE | `/admin/news/{slug}` | Да | Удалить новость. |
### GET /api/v1/news
**Query-параметры**: `limit` (default 20), `page` (default 1, ≥1).
**Ответ**: `Page<NewsDTO>`, где `items` отсортированы по `publishedAt` у опубликованных записей.
### GET /api/v1/news/{slug}
Возвращает `NewsDTO` опубликованной новости (если slug неактивен — `404`).
### GET /api/v1/admin/news
**Query-параметры**: `limit` (50), `page` (1, ≥1).
**Ответ**: `Page<NewsDTO>` (включая черновики).
### POST /api/v1/admin/news
**Тело запроса** `NewsCreate`:
| Поле | Тип | Обязательно | Примечание |
| --- | --- | --- | --- |
| `title` | string | да | Заголовок. |
| `slug` | string | да | Уникальный slug (валидация на уровне базы). |
| `summary` | string | да | Краткое описание. |
| `content` | string | да | Полный текст (HTML/Markdown). |
| `status` | string | нет | `draft` (по умолчанию) \| `published` \| `archived`. |
| `imageUrl` | string | нет | Ссылка на обложку. |
**Ответ**: `{ "id": <number> }`.
### PUT /api/v1/admin/news/{slug}
**Тело запроса** `NewsUpdateRequest` (все поля опциональны, как в `NewsCreate`). Ответ `{ "updated": true }`.
### POST /api/v1/admin/news/{slug}/publish
Без тела. Устанавливает `status = "published"`, `publishedAt = now`. Ответ `{ "published": true }`.
### DELETE /api/v1/admin/news/{slug}
Ответ `{ "deleted": true }`.
---
## Категории услуг
| Метод | Путь | JWT | Описание |
| --- | --- | --- | --- |
| GET | `/service-categories` | Нет | Публичный список категорий. |
| GET | `/service-categories/{slug}` | Нет | Получить категорию по slug. |
| GET | `/admin/service-categories` | Да | Пагинация категорий. |
| POST | `/admin/service-categories` | Да | Создать категорию. |
| PUT | `/admin/service-categories/{id}` | Да | Обновить категорию. |
| DELETE | `/admin/service-categories/{id}` | Да | Удалить категорию. |
### GET /api/v1/service-categories
Возвращает массив `ServiceCategoryDTO`.
### GET /api/v1/service-categories/{slug}
Ответ `ServiceCategoryDTO`.
### GET /api/v1/admin/service-categories
**Query-параметры**: `limit` (100), `offset` (0).
Ответ: список `ServiceCategoryDTO` (без обёртки).
### POST /api/v1/admin/service-categories
**Тело запроса** `CategoryCreateRequest` (`name`, `slug` обязательные). Ответ `{ "id": <number> }`.
### PUT /api/v1/admin/service-categories/{id}
Тело `CategoryUpdateRequest` (оба поля опциональны). Ответ `{ "updated": true }`.
### DELETE /api/v1/admin/service-categories/{id}
Ответ `{ "deleted": true }`.
---
## Услуги
| Метод | Путь | JWT | Описание |
| --- | --- | --- | --- |
| GET | `/services` | Нет | Список опубликованных услуг с фильтрами. |
| GET | `/services/{slug}` | Нет | Одна услуга по slug. |
| GET | `/admin/services` | Да | Список всех услуг. |
| POST | `/admin/services` | Да | Создать услугу. |
| PUT | `/admin/services/{id}` | Да | Обновить услугу. |
| PUT | `/admin/services/{id}/status` | Да | Изменить статус. |
| DELETE | `/admin/services/{id}` | Да | Удалить услугу. |
### GET /api/v1/services
**Query-параметры**
| Имя | Тип | По умолчанию | Описание |
| --- | --- | --- | --- |
| `limit` | integer | 20 | Размер страницы. |
| `page` | integer | 1 | ≥ 1. |
| `q` | string | null | Поиск по `title` и `description` (case-insensitive). |
| `category` | string | null | `slug` категории. |
| `minPrice` | number | null | Минимальная цена (decimal). |
| `maxPrice` | number | null | Максимальная цена (decimal). |
**Ответ**: `Page<ServiceDTO>`.
Пример элемента:
```json
{
"id": 10,
"title": "Разработка мобильного приложения",
"slug": "mobile-dev",
"description": "Полный цикл разработки",
"priceFrom": 150000.0,
"imageUrl": "https://cdn.example/app.jpg",
"status": "PUBLISHED",
"category": { "id": 2, "name": "Разработка", "slug": "development" },
"createdAt": "2024-04-01T09:15:00",
"updatedAt": "2024-05-05T12:00:00"
}
```
### GET /api/v1/services/{slug}
Ответ `ServiceDTO` (включая `category`). Ошибка `404`, если slug не найден или услуга скрыта.
### GET /api/v1/admin/services
**Query-параметры**
| Имя | Тип | По умолчанию | Описание |
| --- | --- | --- | --- |
| `limit` | integer | 50 | Размер страницы. |
| `page` | integer | 1 | ≥ 1. |
| `q` | string | null | Фильтр по названию/описанию. |
| `status` | string | null | `PUBLISHED` \| `DRAFT` \| `ARCHIVED`. |
Ответ: `Page<ServiceDTO>` (включая черновики).
### POST /api/v1/admin/services
**Тело запроса** `ServiceCreateRequest`:
| Поле | Тип | Обязательно | Примечание |
| --- | --- | --- | --- |
| `title` | string | да | Название услуги. |
| `slug` | string | да | Уникальный slug (`^[a-z0-9-]{3,}$`). |
| `description` | string | да | Детальное описание. |
| `priceFrom` | string | нет | Десятичное число, например `"1499.99"`. |
| `imageUrl` | string | нет | URL изображения. |
| `status` | string | нет | По умолчанию `PUBLISHED`. |
| `categoryId` | number | нет | ID существующей категории. |
**Ответ 201**: `{ "id": <number> }`.
### PUT /api/v1/admin/services/{id}
Тело `ServiceUpdateRequest` (все поля опциональны, формат как в `ServiceCreateRequest`). Ответ `{ "updated": true }`.
### PUT /api/v1/admin/services/{id}/status
**Тело запроса** `StatusRequest` с полем `status` (`PUBLISHED`\|`DRAFT`\|`ARCHIVED`). Ответ `{ "updated": true }`.
### DELETE /api/v1/admin/services/{id}
Ответ `{ "deleted": true }`.
---
## Пример структуры Page
```json
{
"items": [ /* сущности */ ],
"total": 125,
"limit": 20,
"offset": 0
}
```