axe/docs/swagger.json
2026-10-18 22:07:25 +03:00

4766 lines
196 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"schemes": [
"http"
],
"swagger": "2.0",
"info": {
"description": "Управление заявками на обслуживание: админ создаёт клиента, клиент заводит проект, объекты в нём и мастеров, назначает заявку мастеру проекта, мастер выполняет, клиент подтверждает работу или отклоняет её на доработку. Финальный статус — closed.",
"title": "Building Management API",
"contact": {},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/",
"paths": {
"/auth/forgot-password": {
"post": {
"description": "Отправляет на email письмо со ссылкой формы сброса (токен живёт 24 часа, одноразовый). Ответ всегда одинаковый — 200 с ok:true, независимо от того, существует ящик, подтверждён он и активен: эндпоинт не перечисляет аккаунты. Письмо уходит только подтверждённым активным аккаунтам. Лимиты: 10 запросов в минуту с одного IP; 3 письма в сутки на один ящик (счётчик тикает при любом запросе — и для несуществующих ящиков тоже), при превышении — 429.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Запрос письма для сброса пароля",
"parameters": [
{
"description": "Email для восстановления",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.forgotPasswordReq"
}
}
],
"responses": {
"200": {
"description": "всегда ok:true, даже если ящик не найден",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректное тело запроса",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"429": {
"description": "слишком много попыток",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/login": {
"post": {
"description": "Возвращает access-токен; refresh-токен — только в httpOnly-куке (Path=/auth, SameSite=Lax, Secure — через COOKIE_SECURE), в JSON его нет. Email ищется без учёта регистра, пароль — bcrypt. Неподтверждённый аккаунт — 403 account not confirmed (отличайте от account disabled). Лимит: 10 попыток в минуту с одного IP.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Вход",
"parameters": [
{
"description": "Учётные данные",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.loginReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TokenResponse"
}
},
"400": {
"description": "некорректное тело запроса",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "неверный логин или пароль",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "аккаунт отключён либо не подтверждён",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"429": {
"description": "слишком много попыток",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/logout": {
"post": {
"description": "Гасит refresh-сессию из куки refresh_token и очищает куку. Тело запроса не нужно. Auth-middleware намеренно не используется: выйти должен пользователь и с протухшим access-токеном.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Выйти (одно устройство)",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"429": {
"description": "слишком много попыток",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/logout-all": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Закрывает все сессии текущего пользователя.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Выйти со всех устройств",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/me": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Текущий пользователь",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Profile"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/refresh": {
"post": {
"description": "Refresh-токен читается из httpOnly-куки refresh_token (Path=/auth), тело запроса не требуется. Ротация: на выходе новый access-токен и новая кука, старая сессия гасится. Повторное использование отозванного токена закрывает все сессии пользователя — признак того, что секретом владеет кто-то второй.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Обновить пару токенов",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TokenResponse"
}
},
"401": {
"description": "куки нет, токен неизвестен, отозван или истёк",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "аккаунт отключён",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"429": {
"description": "слишком много попыток",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/reset-password": {
"post": {
"description": "Токен одноразовый и живёт 24 часа: приходит письмом после POST /auth/forgot-password либо выдаётся админом через POST /users/{id}/password-reset. Новый пароль — по политике (10..72 символа, буквы и цифры). Успех гасит токен и все refresh-сессии пользователя. Лимит: 30 попыток в минуту с одного IP.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Сброс пароля по токену",
"parameters": [
{
"description": "Токен и новый пароль",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.resetPasswordReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректное тело или токен неизвестен, использован либо истёк; слабый пароль",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"429": {
"description": "слишком много попыток",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/telegram": {
"get": {
"description": "Куда сейчас приходят уведомления о заявках: цель (чат или группа), имя бота (свой или глобальный), подключён ли собственный токен бота (has_bot_token) и можно ли его ввести (token_configurable — зависит от DATA_ENCRYPTION_KEY на сервере). Сам токен наружу не отдаётся.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Статус уведомлений в Telegram",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TelegramStatusResponse"
}
},
"401": {
"description": "нет входа",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "профиль не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Принимает @имя группы/канала (бот обязан быть в нём) или числовой chat_id; пустое значение отключает уведомления. Сначала проверяется формат, затем бот проверяет чат через getChat — чужой или недоступный чат не сохраняется.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Указать цель уведомлений вручную",
"parameters": [
{
"description": "Цель: @группа, chat_id или пусто",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.setTelegramReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "invalid telegram target",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет входа",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Очищает цель уведомлений текущего пользователя (то же, что PUT с пустым значением). Токен бота при этом не трогается — он живёт на своём эндпоинте.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Отключить уведомления в Telegram",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"401": {
"description": "нет входа",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/telegram/bot": {
"post": {
"description": "Принимает токен Bot API (формат «id:secret» от @BotFather), проверяет его через getMe, шифрует и сохраняет. Дальше уведомления пользователя идут через этого бота, его поллер поднимается сразу. Лимит: 5 попыток за 5 минут. Без DATA_ENCRYPTION_KEY на сервере — 400.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Подключить своего бота",
"parameters": [
{
"description": "Токен бота",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.setBotReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "токен не сохранён (формат, getMe или фича выключена)",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет входа",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "профиль не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"429": {
"description": "слишком много попыток",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Стирает сохранённый токен текущего пользователя и останавливает его поллер. Повторный вызов безопасен — ответ тот же 200.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Отключить своего бота",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"401": {
"description": "нет входа",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "профиль не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/auth/telegram/code": {
"post": {
"description": "Одноразовый код и deep-link t.me/{bot}?start={code}: откройте ссылку в Telegram и отправьте боту код — этот чат начнёт получать уведомления о заявках. Бот — ваш собственный токен, а если его нет — глобальный бот сервера. Код живёт 15 минут, прежние коды гаснут. Лимит: 5 выпусков за 5 минут на пользователя.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Выдать код привязки Telegram",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.BindCodeResponse"
}
},
"400": {
"description": "ни свой бот, ни глобальный не настроены",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "профиль не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"429": {
"description": "слишком много кодов",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/invitations": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Список приглашений для текущего мастера: по каждому — проект, кто пригласил и до какого срока. Просроченные сюда не попадают — принять их уже нельзя.",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Мои приглашения в проекты",
"parameters": [
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"invited_at",
"expires_at",
"project_name"
],
"type": "string",
"default": "invited_at",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.InvitationListResponse"
}
},
"400": {
"description": "некорректные параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/objects/{id}": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "Поля опциональны. У уже созданных заявок адрес остаётся тем, что был на момент создания — это осознанно.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Изменить объект",
"parameters": [
{
"type": "integer",
"description": "ID объекта",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Что изменить",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateObjectReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Object"
}
},
"400": {
"description": "некорректный id или пустое тело",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "объект не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "имя уже занято в проекте",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/projects": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Клиент видит только свои проекты, админ — все (можно сузить через client_id). Мастерам проекты не отдаются. Ответ — страница: items плюс total/limit/offset.",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Список проектов",
"parameters": [
{
"type": "string",
"description": "Фильтр по клиенту (только admin)",
"name": "client_id",
"in": "query"
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"created_at",
"name",
"id"
],
"type": "string",
"default": "created_at",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.ProjectListResponse"
}
},
"400": {
"description": "некорректные параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Клиент создаёт проект себе; админ — для любого клиента (в этом случае обязателен client_id). Проект — контур, внутри которого живут объекты и мастера.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Создать проект",
"parameters": [
{
"description": "Данные проекта",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.createProjectReq"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/models.Project"
}
},
"400": {
"description": "некорректное тело; client_id обязателен для admin; владелец не клиент",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "у клиента уже есть проект с таким именем",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/projects/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Админ — любой проект, клиент — только свой; чужой проект неотличим от несуществующего (404).",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Детали проекта",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Project"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"description": "Действие необратимо: вместе с проектом удаляются его объекты, участия мастеров и все заявки (вместе с событиями и фото). Объекты фото в хранилище убираются после успешного удаления в БД. Клиент своих проектов не удаляет — только админ.",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Удалить проект",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "Поля опциональны, меняется только переданное. Выключенный проект не принимает новые заявки и объекты.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Изменить проект",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Что изменить",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateProjectReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Project"
}
},
"400": {
"description": "некорректный id или пустое тело",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "имя уже занято у этого клиента",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/projects/{id}/invitation/accept": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "invited → active: с этого момента мастер состоит в проекте и ему можно назначать заявки. Действует только на своё непросроченное приглашение (срок — 7 дней, см. repository.InvitationTTL).",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Принять приглашение в проект",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "приглашения нет",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "срок вышел или приглашение уже закрыто",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/projects/{id}/invitation/decline": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "invited → declined: заявок мастер не получает, клиент видит отказ в составе проекта и может пригласить повторно (строка та же, история ответа обновляется).",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Отклонить приглашение в проект",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "приглашения нет",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "приглашение уже закрыто",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/projects/{id}/objects": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Здания/корпуса, входящие в проект. Адрес заявки берётся из объекта.",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Объекты проекта",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"name",
"created_at",
"id"
],
"type": "string",
"default": "name",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.ObjectListResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Объект — здание/корпус с адресом; заявки создаются именно на объект. Выключенный проект объектов не принимает.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Создать объект",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Данные объекта",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.createObjectReq"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/models.Object"
}
},
"400": {
"description": "некорректный id или тело",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "проект выключен или имя объекта занято",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/projects/{id}/workers": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Кто приглашён в проект и кем принят: статус invited — приглашение ждёт решения, active — мастер работает с заявками, declined — отказал. Хеши паролей в ответ не попадают.",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Мастера проекта",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"name",
"created_at",
"email",
"id"
],
"type": "string",
"default": "name",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.ProjectWorkerListResponse"
}
},
"400": {
"description": "некорректный id или параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "С 0005 это приглашение, а не готовое членство: мастер становится участником только сам, приняв приглашение (POST /projects/{id}/invitation/accept) — до этого заявки проекта ему не назначаются. Приглашение живёт 7 дней. Мастер может состоять в нескольких проектах; аккаунт должен быть активным, с ролью worker и подтверждённым (0010) — неподтверждённого сначала подтверждает тот, кто его создал.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Пригласить мастера в проект",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Мастер",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.addWorkerReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.InvitationResponse"
}
},
"400": {
"description": "некорректный id/тело; не мастер, неактивен или не подтверждён",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект или мастер не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "мастер уже состоит в проекте / приглашение уже ждёт ответа",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/projects/{id}/workers/{worker_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"description": "Нельзя убрать, пока за ним закреплены незакрытые заявки проекта (409) — иначе работа осталась бы без исполнителя. Заявки из истории при этом не теряются.",
"produces": [
"application/json"
],
"tags": [
"projects"
],
"summary": "Убрать мастера из проекта",
"parameters": [
{
"type": "integer",
"description": "ID проекта",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "ID мастера (uuid)",
"name": "worker_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "проект не найден или мастер не состоит в проекте",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "за мастером есть незакрытые заявки",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/recurring": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Клиент видит только свои правила, админ — все. Ответ — страница: items плюс total/limit/offset.",
"produces": [
"application/json"
],
"tags": [
"recurring"
],
"summary": "Список правил повторения",
"parameters": [
{
"type": "integer",
"description": "Сузить до одного проекта",
"name": "project_id",
"in": "query"
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"created_at",
"id",
"date"
],
"type": "string",
"default": "created_at",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.RecurringRuleListResponse"
}
},
"400": {
"description": "некорректные параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Правило — шаблон заявки (объект, категория, описание, приоритет) плюс расписание: anchor_at (дата-время первого запуска) и period_days — «каждые N дней/недель». Фоновый планировщик сам создаёт заявку, когда срок наступил, — но только если прошлая по правилу уже закрыта (без дублей). Клиент создаёт правило для своего проекта, админ — для любого. Обычно правило заводится чекбоксом в POST /tickets; этот эндпоинт — для управления без заявки.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"recurring"
],
"summary": "Создать правило повторяющейся заявки",
"parameters": [
{
"description": "Данные правила",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.createRecurringReq"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/models.RecurringRule"
}
},
"400": {
"description": "некорректное тело запроса",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "объект не найден (или чужой — неотличим)",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "проект или объект выключен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/recurring/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Админ — любое правило, клиент — только своё; чужое неотличимо от несуществующего (404).",
"produces": [
"application/json"
],
"tags": [
"recurring"
],
"summary": "Детали правила повторения",
"parameters": [
{
"type": "integer",
"description": "ID правила",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.RecurringRule"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "правило не найдено",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"description": "Удаляется само правило, его история срабатываний (какая заявка создавалась и когда) и — каскадом — будущие заявки ещё не созданы. Созданные ранее заявки не трогаются.",
"produces": [
"application/json"
],
"tags": [
"recurring"
],
"summary": "Удалить правило повторения",
"parameters": [
{
"type": "integer",
"description": "ID правила",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "правило не найдено",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "Частичное обновление: применяются только присланные поля, рамки те же, что при создании (anchor_at — любая дата-время, period_days 1..3650). object_id не меняется (перенос — через удаление и создание). Срок не сдвигается вручную: он пересчитывается от последнего срабатывания с новым расписанием.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"recurring"
],
"summary": "Изменить правило повторения",
"parameters": [
{
"type": "integer",
"description": "ID правила",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Что изменить (хотя бы одно поле)",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateRecurringReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.RecurringRule"
}
},
"400": {
"description": "некорректный id, пустое тело или значение вне рамок",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "правило не найдено",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/recurring/{id}/run": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Заявка создаётся сразу, без ожидания интервала. Остальные рубежи как у планировщика: правило, проект и объект включены, прошлая заявка по правилу закрыта — иначе 409, дубль не создаём. Срок сдвигается на момент запуска (следующий фоновый тик отсчитает интервал от него), уведомление уходит тем же текстом, что при фоновом срабатывании.",
"produces": [
"application/json"
],
"tags": [
"recurring"
],
"summary": "Запустить правило сейчас",
"parameters": [
{
"type": "integer",
"description": "ID правила",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handlers.RecurringRunResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "правило не найдено (или чужое — неотличим)",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "правило, проект или объект выключены, либо есть открытая заявка",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/recurring/{id}/runs": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Какие заявки создавались по правилу и когда (аудит). Сначала свежие. Строка живёт вместе со своей заявкой: когда заявку удаляет автоочистка (TICKET_RETENTION_DAYS), строка истории уходит следом.",
"produces": [
"application/json"
],
"tags": [
"recurring"
],
"summary": "История срабатываний правила",
"parameters": [
{
"type": "integer",
"description": "ID правила",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"run_at",
"id"
],
"type": "string",
"default": "run_at",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.RecurringRunListResponse"
}
},
"400": {
"description": "некорректный id или параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client или admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "правило не найдено",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Выборка режется по роли: клиент видит свои, мастер — назначенные ему, админ — всё. Клиенту и мастеру параметры client_id/worker_id игнорируются: подсмотреть чужое нельзя. Фильтры накладываются поверх выборки по роли. Ответ — страница: items плюс total/limit/offset.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Список заявок",
"parameters": [
{
"enum": [
"new",
"assigned",
"in_progress",
"done",
"confirmed",
"rejected",
"closed"
],
"type": "string",
"description": "Фильтр по статусу",
"name": "status",
"in": "query"
},
{
"enum": [
"electric",
"plumbing",
"climate",
"elevator",
"other"
],
"type": "string",
"description": "Фильтр по категории",
"name": "category",
"in": "query"
},
{
"enum": [
"low",
"normal",
"high"
],
"type": "string",
"description": "Фильтр по приоритету",
"name": "priority",
"in": "query"
},
{
"type": "string",
"description": "Фильтр по клиенту (только admin)",
"name": "client_id",
"in": "query"
},
{
"type": "string",
"description": "Фильтр по мастеру (только admin)",
"name": "worker_id",
"in": "query"
},
{
"type": "integer",
"description": "Фильтр по проекту",
"name": "project_id",
"in": "query"
},
{
"type": "integer",
"description": "Фильтр по объекту",
"name": "object_id",
"in": "query"
},
{
"type": "string",
"description": "created_at с, YYYY-MM-DD (включительно, с полуночи UTC)",
"name": "date_from",
"in": "query"
},
{
"type": "string",
"description": "created_at по, YYYY-MM-DD (включительно, до полуночи UTC следующего дня)",
"name": "date_to",
"in": "query"
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"created_at",
"priority",
"id"
],
"type": "string",
"default": "created_at",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TicketListResponse"
}
},
"400": {
"description": "некорректный фильтр или параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "неизвестная роль",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Клиент создаёт заявку на объект своего проекта. Адрес берётся из объекта на момент создания, статус всегда new, мастер не назначен. Если передан recurring — той же операцией создаётся правило повторения (anchor_at — первый запуск, period_days — период в днях), связанное с заявкой: оно видно в GET /tickets/{id} в поле recurring, управление — через PATCH /recurring/{id}.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Создать заявку",
"parameters": [
{
"description": "Данные заявки",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.createTicketReq"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректное тело запроса",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль client",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "объект не найден (или чужой — неотличим)",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "проект или объект выключен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Админ — любая заявка, клиент — только своя, мастер — назначенная ему либо свободная (new) из его проектов.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Детали заявки",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "заявка не принадлежит вам",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/assign": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "new → assigned. Доступно админу и владельцу-клиенту. Мастер должен состоять в проекте заявки и быть активным — иначе 400/409.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Назначить мастера",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Мастер",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.assignReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id/тело, не-мастер, неактивен, приглашение не принято или не состоит в проекте заявки",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль admin или client; чужая заявка",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "заявка уже назначена или закрыта",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/cancel": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "Закрывает заявку с cancel_reason и closed_by. Клиент — только свои в new/assigned/done/rejected; админ — любую кроме уже закрытой, включая in_progress. Причина обязательна.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Отменить заявку (финал)",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Причина отмены, минимум 5 символов",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.reasonReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id или тело: причина обязательна",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "чужая заявка или роль не client/admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "нельзя закрыть из текущего статуса",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/confirm": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "done → closed с confirmed_at. Доступно только владельцу-клиенту.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Подтвердить работу (финал)",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "заявка не принадлежит вам",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "заявка не в статусе done",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/done": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "in_progress → done. Дальше клиент подтверждает или отклоняет.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Завершить работу",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "не тот статус или заявка назначена другому",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/events": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Хронология переходов заявки с авторами: created, assigned, started, done, confirmed, rejected, canceled. from_status у события «создана» нет. События пишутся той же SQL-операцией, что и переход, поэтому история не расходится со статусом.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Журнал событий заявки",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"id",
"created_at"
],
"type": "string",
"default": "id",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TicketEventListResponse"
}
},
"400": {
"description": "некорректный id или параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "чужая заявка",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/photos": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Файлы заявки в хронологии (id ASC по умолчанию). В каждом элементе url — временная подписанная ссылка на чтение: бакет приватный, сам ключ наружу не уходит.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Список фото отчёта",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"id",
"created_at"
],
"type": "string",
"default": "id",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.PhotoListResponse"
}
},
"400": {
"description": "некорректный id или параметры страницы",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "чужая заявка",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "multipart/form-data, поле file: только фото (jpeg/png/webp, определяется по сигнатуре байтов), не больше PHOTO_MAX_BYTES на файл и PHOTO_MAX_PER_TICKET файлов на заявку. Доступно мастеру (заявка in_progress — после «Работа выполнена» отчёт заморожен, мастер — активный участник проекта) или админу. Каждая загрузка оставляет событие photo_added в журнале заявки.",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Загрузить фото отчёта",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
},
{
"type": "file",
"description": "Файл фотографии (jpeg/png/webp)",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"201": {
"description": "фото загружено, url — временная ссылка на чтение",
"schema": {
"$ref": "#/definitions/models.TicketPhoto"
}
},
"400": {
"description": "некорректный id, файл отсутствует, не фото или больше лимита",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker или admin; чужая заявка",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "лимит файлов исчерпан или заявка не в статусе in_progress",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "хранилище недоступно",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/photos/{photo_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"description": "Своё фото удаляет автор, пока заявка в работе (in_progress): после «Работа выполнена» отчёт заморожен; админу — любое фото. Событие photo_removed пишется той же операцией, что и удаление.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Удалить фото",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "ID фото",
"name": "photo_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id или photo_id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker или admin; чужая заявка",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка или фото не найдены",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "чужое фото или заявка уже не в работе",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/reject": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "done → rejected. Не финал: мастер берёт заявку в работу повторно, rework_count увеличивается. Причина обязательна — без неё мастер не поймёт, что переделывать.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Отклонить работу (на доработку)",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Причина отклонения, минимум 5 символов",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.reasonReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id или тело: причина обязательна",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "заявка не принадлежит вам",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "заявка не в статусе done",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/start": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "assigned → in_progress, а после отклонения (rejected) — повторный запуск. started_at первого круга сохраняется.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Взять в работу",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "не тот статус или заявка назначена другому",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tickets/{id}/take": {
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "new → assigned: мастер назначает себя на свободную заявку своего проекта. Дальше — «Взять в работу» (start) как обычно.",
"produces": [
"application/json"
],
"tags": [
"tickets"
],
"summary": "Взять заявку самому",
"parameters": [
{
"type": "integer",
"description": "ID заявки",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Ticket"
}
},
"400": {
"description": "некорректный id; приглашение не принято или отклонено; не состоит в проекте заявки",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль worker",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "заявка не найдена",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "заявка уже взята или не в статусе new",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/users": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Админ видит любую роль и всех; клиенту доступна только роль worker и только его собственные мастера (чужие организации не видны). С параметром q (минимум 2 символа) возвращается поиск по имени/email среди активных — так клиент пригласит в проект существующего мастера, не видя при этом полного списка. include_inactive=1 дополняет список отключёнными аккаунтами — только для админа.",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Список пользователей по роли",
"parameters": [
{
"enum": [
"worker",
"admin",
"client"
],
"type": "string",
"default": "worker",
"description": "Роль",
"name": "role",
"in": "query"
},
{
"type": "string",
"description": "Поиск по имени или email, минимум 2 символа",
"name": "q",
"in": "query"
},
{
"enum": [
"0",
"1"
],
"type": "string",
"default": "0",
"description": "Показать и отключённых (только для админа)",
"name": "include_inactive",
"in": "query"
},
{
"type": "integer",
"default": 20,
"description": "Строк на странице, 1..100",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Сдвиг от начала выборки",
"name": "offset",
"in": "query"
},
{
"enum": [
"name",
"created_at",
"email",
"id"
],
"type": "string",
"default": "name",
"description": "Поле сортировки",
"name": "sort",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Направление сортировки",
"name": "order",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.ProfileListResponse"
}
},
"400": {
"description": "неизвестная роль, параметры страницы или короткий q",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "клиент запрашивает не мастеров",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Админ создаёт мастеров, админов и клиентов; клиент — только мастеров (для себя, далее мастер добавляется в проект через POST /projects/{id}/workers). Роли admin и client клиентом недоступны. Пароль — по политике (10..72 символа, буквы и цифры). Подтверждение: аккаунт, созданный админом, подтверждён сразу; созданный клиентом — ждёт подтверждения этим же клиентом, после чего мастеру уходит письмо.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Создать пользователя",
"parameters": [
{
"description": "Данные пользователя",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.createUserReq"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/models.Profile"
}
},
"400": {
"description": "некорректное тело запроса или слабый пароль",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "клиент пытается создать админа или клиента",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "email уже занят",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/users/{id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"description": "Админ удаляет любой «чистый» аккаунт: refresh-сессии и токены сброса удаляются каскадом, но наличие заявок, проектов или участий блокирует удаление (409 user has related records) — в этом случае отключите аккаунт через PATCH /users/{id}, историю не стираем. Клиент удаляет только мастера, состоящего в одном из его проектов: членства клиента в проектах удаляются в той же транзакции, но если на мастера ссылаются заявки либо чужие проекты — тоже 409, ничего не меняется. Чужой мастер и несуществующий id неразличимы (404). Свой id удалить нельзя.",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Удалить пользователя",
"parameters": [
{
"type": "string",
"description": "ID пользователя (uuid)",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id; нельзя удалить себя",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль admin или client",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "пользователь не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "у пользователя есть связанные записи",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"patch": {
"security": [
{
"Bearer": []
}
],
"description": "Админ меняет любой аккаунт; клиент — только мастера, который активный участник одного из его проектов (иного клиента или чужого мастера не видит — 404). При деактивации все refresh-сессии пользователя закрываются сразу, мастер перестаёт работать во всех проектах. Выключить самого себя нельзя — вы остались бы без входа.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Включить/выключить аккаунт",
"parameters": [
{
"type": "string",
"description": "ID пользователя (uuid)",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Новый флаг",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.setUserReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Profile"
}
},
"400": {
"description": "некорректный id или тело; попытка выключить себя",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль admin или client",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "пользователь не найден (для клиента — не его мастер)",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/users/{id}/confirm": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Подтверждает неподтверждённый аккаунт и шлёт пользователю письмо «аккаунт подтверждён» со ссылкой на вход. Право — в SQL: админ подтверждает любой аккаунт, клиент — только созданного им мастера (иной и несуществующий не видны — 404). Повторное подтверждение — 409. Письмо отправляется после подтверждения; недоступный SMTP не откатывает подтверждение (письмо просто не уйдёт).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Подтвердить аккаунт",
"parameters": [
{
"type": "string",
"description": "ID пользователя (uuid)",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль admin или client",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "пользователь не найден (для клиента — не созданный им мастер)",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"409": {
"description": "аккаунт уже подтверждён",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/users/{id}/password-reset": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Токен возвращается в ответе — админ передаёт его пользователю сам, любым каналом. Для пользователя есть почта: POST /auth/forgot-password шлёт письмо со ссылкой. Токен одноразовый, живёт 24 часа (repository.ResetTTL); выпуск нового гасит прежние неиспользованные токены пользователя (включая свежие из писем). В БД хранится только sha256 токена.",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Выдать токен сброса пароля",
"parameters": [
{
"type": "string",
"description": "ID пользователя (uuid)",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.PasswordResetResponse"
}
},
"400": {
"description": "некорректный id",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "пользователь не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/users/{id}/password-set": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Админ ставит пароль пользователю сам, без ссылки. Политика та же, что при создании и при сбросе по токену (10..72 символа, буквы и цифры). Успех закрывает все refresh-сессии пользователя и гасит неиспользованные токены сброса — со старым паролем в системе остаться нельзя, старые ссылки перестают работать. Альтернатива — одноразовая ссылка через POST /users/{id}/password-reset.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Задать пароль напрямую",
"parameters": [
{
"type": "string",
"description": "ID пользователя (uuid)",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Новый пароль",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.setPasswordReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.OKResponse"
}
},
"400": {
"description": "некорректный id, тело или слабый пароль",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"401": {
"description": "нет или неверный токен",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "нужна роль admin",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "пользователь не найден",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
}
},
"definitions": {
"handlers.BindCodeResponse": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"expires_at": {
"type": "string"
},
"link": {
"type": "string"
}
}
},
"handlers.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"handlers.InvitationListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.ProjectInvitation"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.InvitationResponse": {
"type": "object",
"properties": {
"expires_at": {
"description": "До какого срока мастер может ответить (см. repository.InvitationTTL).",
"type": "string"
},
"ok": {
"type": "boolean"
},
"status": {
"description": "Всегда \"invited\": стать участником мастер может только сам, через\nPOST /projects/{id}/invitation/accept.",
"type": "string"
}
}
},
"handlers.OKResponse": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"revoked": {
"description": "Revoked — сколько сессий закрыто; заполняется только в logout-all.",
"type": "integer"
}
}
},
"handlers.ObjectListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Object"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.PasswordResetResponse": {
"type": "object",
"properties": {
"expires_at": {
"description": "См. repository.ResetTTL (24 часа).",
"type": "string"
},
"ok": {
"type": "boolean"
},
"token": {
"type": "string"
}
}
},
"handlers.PhotoListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.TicketPhoto"
}
},
"limit": {
"type": "integer"
},
"max_per_ticket": {
"description": "MaxPerTicket — лимит файлов на заявку (PHOTO_MAX_PER_TICKET):\nUI рисует «n / N», не зная настройки окружения.",
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.ProfileListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Profile"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.ProjectListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Project"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.ProjectWorkerListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.ProjectWorker"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.RecurringRuleListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.RecurringRule"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.RecurringRunListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.RecurringRun"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.RecurringRunResponse": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"ticket_id": {
"type": "integer"
}
}
},
"handlers.TelegramStatusResponse": {
"type": "object",
"properties": {
"bot_username": {
"description": "BotUsername — @имя бота, через который придут уведомления и\nссылка привязки; пусто, пока getMe не удался.",
"type": "string"
},
"bound": {
"description": "Bound — есть ли уже цель уведомлений.",
"type": "boolean"
},
"enabled": {
"description": "Enabled — может ли этот пользователь получать уведомления вообще:\nесть свой токен бота либо настроен глобальный бот сервера.",
"type": "boolean"
},
"has_bot_token": {
"description": "HasBotToken — подключён ли пользователь свой бот (токен в БД).",
"type": "boolean"
},
"target": {
"description": "Target — текущая цель: числовой chat_id либо @группа.",
"type": "string"
},
"token_configurable": {
"description": "TokenConfigurable — можно ли ввести токен прямо сейчас: false,\nесли на сервере не задан DATA_ENCRYPTION_KEY.",
"type": "boolean"
}
}
},
"handlers.TicketEventListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.TicketEvent"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.TicketListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Ticket"
}
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.TokenResponse": {
"type": "object",
"properties": {
"expires_in": {
"description": "Срок жизни access-токена в секундах.",
"type": "integer"
},
"token": {
"description": "Access-токен: JWT на auth.AccessTTL.",
"type": "string"
},
"user": {
"$ref": "#/definitions/models.Profile"
}
}
},
"handlers.addWorkerReq": {
"type": "object",
"required": [
"worker_id"
],
"properties": {
"worker_id": {
"type": "string"
}
}
},
"handlers.assignReq": {
"type": "object",
"required": [
"worker_id"
],
"properties": {
"worker_id": {
"type": "string"
}
}
},
"handlers.createObjectReq": {
"type": "object",
"required": [
"address",
"name"
],
"properties": {
"address": {
"type": "string",
"minLength": 5
},
"name": {
"type": "string",
"minLength": 2
}
}
},
"handlers.createProjectReq": {
"type": "object",
"required": [
"name"
],
"properties": {
"client_id": {
"description": "ClientID — только для админа: для какого клиента создаётся проект.\nУ клиента владелец берётся из токена, поле игнорируется.",
"type": "string"
},
"name": {
"type": "string",
"minLength": 2
}
}
},
"handlers.createRecurringReq": {
"type": "object",
"required": [
"anchor_at",
"category",
"description",
"object_id",
"period_days"
],
"properties": {
"anchor_at": {
"description": "Расписание: «каждые PeriodDays дней/недель от AnchorAt» —\nAnchorAt (RFC3339) — дата-время первого запуска, PeriodDays —\nпериод в днях (1..3650; недели кратны 7).",
"type": "string"
},
"category": {
"type": "string",
"enum": [
"electric",
"plumbing",
"climate",
"elevator",
"other"
]
},
"description": {
"type": "string",
"minLength": 5
},
"floor": {
"type": "string"
},
"object_id": {
"type": "integer"
},
"period_days": {
"type": "integer",
"maximum": 3650,
"minimum": 1
},
"priority": {
"type": "string",
"enum": [
"low",
"normal",
"high"
]
}
}
},
"handlers.createTicketReq": {
"type": "object",
"required": [
"category",
"description",
"object_id"
],
"properties": {
"category": {
"type": "string",
"enum": [
"electric",
"plumbing",
"climate",
"elevator",
"other"
]
},
"description": {
"type": "string",
"minLength": 5
},
"floor": {
"type": "string"
},
"object_id": {
"description": "Объект, на который создаётся заявка: адрес копируется из объекта.",
"type": "integer"
},
"priority": {
"type": "string",
"enum": [
"low",
"normal",
"high"
]
},
"recurring": {
"description": "Recurring — задан, если заявку делают повторяющейся.",
"allOf": [
{
"$ref": "#/definitions/handlers.recurringSchedule"
}
]
}
}
},
"handlers.createUserReq": {
"type": "object",
"required": [
"email",
"name",
"password",
"role"
],
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"role": {
"type": "string",
"enum": [
"worker",
"admin",
"client"
]
}
}
},
"handlers.forgotPasswordReq": {
"type": "object",
"required": [
"email"
],
"properties": {
"email": {
"type": "string"
}
}
},
"handlers.loginReq": {
"type": "object",
"required": [
"email",
"password"
],
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"handlers.reasonReq": {
"type": "object",
"required": [
"reason"
],
"properties": {
"reason": {
"type": "string",
"minLength": 5
}
}
},
"handlers.recurringSchedule": {
"type": "object",
"required": [
"anchor_at",
"period_days"
],
"properties": {
"anchor_at": {
"description": "AnchorAt — дата-время первого запуска (RFC3339): точка отсчёта\nсетки сроков, от неё идут «каждые period_days».",
"type": "string"
},
"period_days": {
"description": "PeriodDays — период в днях (1..3650; недели кратны 7).",
"type": "integer",
"maximum": 3650,
"minimum": 1
}
}
},
"handlers.resetPasswordReq": {
"type": "object",
"required": [
"password",
"token"
],
"properties": {
"password": {
"type": "string"
},
"token": {
"type": "string"
}
}
},
"handlers.setBotReq": {
"type": "object",
"properties": {
"token": {
"description": "Token — токен бота от @BotFather; пусто невалидно (нужен явный ввод).",
"type": "string"
}
}
},
"handlers.setPasswordReq": {
"type": "object",
"required": [
"password"
],
"properties": {
"password": {
"type": "string"
}
}
},
"handlers.setTelegramReq": {
"type": "object",
"properties": {
"target": {
"description": "Target — @группа/@канал или числовой chat_id; пусто — отключить.",
"type": "string"
}
}
},
"handlers.setUserReq": {
"type": "object",
"required": [
"is_active"
],
"properties": {
"is_active": {
"description": "*bool обязателен: на bool с binding:\"required\" запрос\n«деактивировать» (is_active=false) не прошёл бы валидацию.",
"type": "boolean"
}
}
},
"handlers.updateObjectReq": {
"type": "object",
"properties": {
"address": {
"type": "string",
"minLength": 5
},
"is_active": {
"type": "boolean"
},
"name": {
"type": "string",
"minLength": 2
}
}
},
"handlers.updateProjectReq": {
"type": "object",
"properties": {
"is_active": {
"type": "boolean"
},
"name": {
"type": "string",
"minLength": 2
}
}
},
"handlers.updateRecurringReq": {
"type": "object",
"properties": {
"anchor_at": {
"type": "string"
},
"category": {
"type": "string"
},
"description": {
"type": "string"
},
"floor": {
"type": "string"
},
"is_active": {
"type": "boolean"
},
"period_days": {
"type": "integer"
},
"priority": {
"type": "string"
}
}
},
"models.Object": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_active": {
"type": "boolean"
},
"name": {
"type": "string"
},
"project_id": {
"type": "integer"
},
"project_name": {
"description": "Денормализации для UI.",
"type": "string"
}
}
},
"models.Profile": {
"type": "object",
"properties": {
"confirmed_at": {
"description": "ConfirmedAt — момент подтверждения аккаунта; nil (null в JSON)\n— ожидает подтверждения, вход до этого запрещён (403\naccount not confirmed). Существующие пользователи подтверждены\nбэкфиллом миграции 0010.",
"type": "string"
},
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"id": {
"type": "string"
},
"is_active": {
"type": "boolean"
},
"name": {
"type": "string"
},
"phone": {
"type": "string"
},
"role": {
"type": "string"
}
}
},
"models.Project": {
"type": "object",
"properties": {
"client_id": {
"type": "string"
},
"client_name": {
"description": "Денормализация для UI: чей проект (админ видит все сразу).",
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_active": {
"type": "boolean"
},
"name": {
"type": "string"
}
}
},
"models.ProjectInvitation": {
"type": "object",
"properties": {
"client_name": {
"type": "string"
},
"expires_at": {
"type": "string"
},
"invited_at": {
"type": "string"
},
"invited_by": {
"description": "id пригласившего",
"type": "string"
},
"invited_by_name": {
"description": "его имя для UI",
"type": "string"
},
"project_id": {
"type": "integer"
},
"project_name": {
"type": "string"
}
}
},
"models.ProjectWorker": {
"type": "object",
"properties": {
"confirmed_at": {
"description": "ConfirmedAt — момент подтверждения аккаунта; nil (null в JSON)\n— ожидает подтверждения, вход до этого запрещён (403\naccount not confirmed). Существующие пользователи подтверждены\nбэкфиллом миграции 0010.",
"type": "string"
},
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"expires_at": {
"description": "ExpiresAt — крайний срок ответа; заполнен у приглашений.",
"type": "string"
},
"id": {
"type": "string"
},
"is_active": {
"type": "boolean"
},
"name": {
"type": "string"
},
"phone": {
"type": "string"
},
"responded_at": {
"description": "RespondedAt — когда мастер принял/отклонил; пусто, пока не ответил.",
"type": "string"
},
"role": {
"type": "string"
},
"status": {
"description": "invited | active | declined (см. 0005_project_invitations.sql).",
"type": "string"
}
}
},
"models.RecurringRule": {
"type": "object",
"properties": {
"address": {
"description": "Адрес объекта — снимок, как у заявки: правило показывает, куда едут.",
"type": "string"
},
"anchor_at": {
"description": "Расписание: «каждые PeriodDays дней/недель от AnchorAt» —\nAnchorAt — дата-время первого запуска (точка отсчёта сетки\nсроков), PeriodDays — период в днях (1..3650, недели кратны 7).",
"type": "string"
},
"category": {
"description": "Шаблон заявки — те же поля, что у tickets.",
"type": "string"
},
"client_id": {
"description": "ClientID — владелец будущих заявок (клиент проекта).",
"type": "string"
},
"client_name": {
"type": "string"
},
"created_at": {
"type": "string"
},
"created_by": {
"description": "CreatedBy — кто завёл правило (админ или клиент); в журнале\nсозданной заявки это же лицо будет автором события «created».",
"type": "string"
},
"created_by_name": {
"description": "Кто завёл правило (админ или сам клиент) — для аудита, в JSON идёт имя.",
"type": "string"
},
"description": {
"type": "string"
},
"floor": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_active": {
"type": "boolean"
},
"last_run_at": {
"description": "LastRunAt — когда последний раз срабатывали; null (в JSON — null)\nозначает «ещё ни разу», следующий запуск считается от created_at.",
"type": "string"
},
"next_run_at": {
"description": "NextRunAt — ближайший запуск после последнего срабатывания\n(вычисляется при чтении; у выключенного правила — когда включат).",
"type": "string"
},
"object_id": {
"type": "integer"
},
"object_name": {
"type": "string"
},
"period_days": {
"type": "integer"
},
"priority": {
"type": "string"
},
"project_id": {
"type": "integer"
},
"project_name": {
"description": "Денормализации для UI.",
"type": "string"
}
}
},
"models.RecurringRun": {
"type": "object",
"properties": {
"address": {
"description": "Адрес и категория — из заявки на момент создания (для списка истории).",
"type": "string"
},
"category": {
"type": "string"
},
"id": {
"type": "integer"
},
"rule_id": {
"type": "integer"
},
"run_at": {
"type": "string"
},
"ticket_id": {
"description": "TicketID — созданная заявка (её номер = id); null, если её удалили\nкаскадом вместе с проектом.",
"type": "integer"
}
}
},
"models.Ticket": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"assigned_at": {
"type": "string"
},
"assigned_by": {
"type": "string"
},
"cancel_reason": {
"type": "string"
},
"category": {
"type": "string"
},
"client_id": {
"type": "string"
},
"client_name": {
"description": "Денормализованные поля для UI — подтягиваются JOIN'ом",
"type": "string"
},
"closed_at": {
"type": "string"
},
"closed_by": {
"type": "string"
},
"confirmed_at": {
"type": "string"
},
"created_at": {
"type": "string"
},
"description": {
"type": "string"
},
"done_at": {
"type": "string"
},
"floor": {
"type": "string"
},
"id": {
"type": "integer"
},
"object_id": {
"type": "integer"
},
"object_name": {
"type": "string"
},
"priority": {
"type": "string"
},
"project_id": {
"description": "Принадлежность: заявка живёт в объекте проекта клиента.\nПроверяется составными FK в БД (миграция 0004).",
"type": "integer"
},
"project_name": {
"type": "string"
},
"recurring": {
"description": "Recurring — правило повторения, если заявка связана с ним:\nсоздана вместе с правилом (чекбокс «Новая заявка») или самим\nпланировщиком. Заполняется только в детали (GetByID) — спискам\nблок на странице заявки не нужен.",
"allOf": [
{
"$ref": "#/definitions/models.TicketRecurring"
}
]
},
"reject_reason": {
"type": "string"
},
"rejected_at": {
"description": "Закрытие цикла: rejected — это доработка, closed — финал.",
"type": "string"
},
"rework_count": {
"description": "Сколько раз клиент отклонял работу — чтобы доработки не бесконечны\nбыли незаметны.",
"type": "integer"
},
"started_at": {
"type": "string"
},
"status": {
"type": "string"
},
"worker_id": {
"type": "string"
},
"worker_name": {
"type": "string"
}
}
},
"models.TicketEvent": {
"type": "object",
"properties": {
"action": {
"description": "created | assigned | started | done | confirmed | rejected |\ncanceled | photo_added | photo_removed.",
"type": "string"
},
"actor_id": {
"description": "\"\" — автор удалён (событие сохранится, имя нет).",
"type": "string"
},
"actor_name": {
"type": "string"
},
"comment": {
"description": "Причина для rejected/canceled, имя файла для photo_*; иначе пусто.",
"type": "string"
},
"created_at": {
"type": "string"
},
"from_status": {
"description": "\"\" — у события «создана» не было предыдущего статуса.\nУ photo_added/photo_removed статус не менялся: оба равны текущему.",
"type": "string"
},
"id": {
"type": "integer"
},
"ticket_id": {
"type": "integer"
},
"to_status": {
"type": "string"
}
}
},
"models.TicketPhoto": {
"type": "object",
"properties": {
"content_type": {
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"original_name": {
"description": "Имя, которое дал файл при загрузке: лежит и в журнале (comment).",
"type": "string"
},
"size_bytes": {
"type": "integer"
},
"ticket_id": {
"type": "integer"
},
"uploader_id": {
"description": "\"\" — аккаунт загрузившего удалён, файл остаётся.",
"type": "string"
},
"uploader_name": {
"type": "string"
},
"url": {
"description": "URL — presigned GET (срок жизни ограничен); заполняется только\nв ответах API. ObjectKey — внутренняя деталь, в JSON не уходит.",
"type": "string"
}
}
},
"models.TicketRecurring": {
"type": "object",
"properties": {
"anchor_at": {
"description": "Расписание: «каждые PeriodDays дней/недель от AnchorAt» —\nточка отсчёта плюс период в днях.",
"type": "string"
},
"is_active": {
"type": "boolean"
},
"next_run_at": {
"description": "NextRunAt — ближайший запуск, вычисляется при чтении\n(см. repository.nextDue); у выключенного правила — когда включат.",
"type": "string"
},
"period_days": {
"type": "integer"
},
"rule_id": {
"type": "integer"
}
}
}
},
"securityDefinitions": {
"Bearer": {
"description": "JWT формата Bearer, получить через POST /auth/login. Срок жизни — 1 час, дальше POST /auth/refresh.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}