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

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