Сервис управления расписаниями дежурств системы Induty.
Отвечает за создание расписаний, управление сменами (shifts) и определение текущего дежурного (on-call).
- Конфигурация
- Запуск
- Миграции
- Модель доступа
- Интеграция с teams service
- API
- GET /healthz /readyz
- POST /api/v1/schedules
- GET /api/v1/schedules
- GET /api/v1/schedules/:id
- PUT /api/v1/schedules/:id
- DELETE /api/v1/schedules/:id
- GET /api/v1/schedules/:id/shifts
- POST /api/v1/schedules/:id/shifts
- PUT /api/v1/schedules/:id/shifts/:shift_id
- DELETE /api/v1/schedules/:id/shifts/:shift_id
- GET /api/v1/schedules/:id/oncall
- GET /api/v1/schedules/oncall
Сервис читает конфиг из config.yaml. Любое поле может быть переопределено переменной окружения.
port: "8004"
database_dsn: "host=localhost user=induty password=secret dbname=induty_schedules port=5432 sslmode=disable"
jwt_secret: "change-me-in-production"
read_timeout: "10s"
write_timeout: "10s"
teams_service_url: "https://teams:8443"
tls_client_cert_file: "./certs/schedules/client.crt"
tls_client_key_file: "./certs/schedules/client.key"
tls_ca_cert_file: "./certs/ca/ca.crt"| Переменная | Описание | Пример |
|---|---|---|
INDUTY_SCHEDULES_PORT |
Порт сервиса | 8004 |
INDUTY_SCHEDULES_DATABASE_DSN |
DSN строка PostgreSQL | host=postgres user=... |
INDUTY_SCHEDULES_JWT_SECRET |
Секрет подписи JWT | supersecret |
INDUTY_SCHEDULES_TEAMS_SERVICE_URL |
Базовый URL teams service | https://teams:8443 |
INDUTY_SCHEDULES_TLS_CLIENT_CERT_FILE |
Путь к клиентскому сертификату для mTLS | /certs/schedules/client.crt |
INDUTY_SCHEDULES_TLS_CLIENT_KEY_FILE |
Путь к клиентскому ключу для mTLS | /certs/schedules/client.key |
INDUTY_SCHEDULES_TLS_CA_CERT_FILE |
Путь к CA сертификату для проверки teams service | /certs/ca/ca.crt |
Переменные окружения имеют приоритет над значениями из config.yaml.
GET /healthzиGET /readyzне требуют авторизации.- Все маршруты
/api/v1/...требуют JWT в заголовкеAuthorization: Bearer <access_token>. - Глобальный
adminимеет полный доступ ко всем расписаниям и сменам. - Для не-
adminдоступ к расписаниям ограничен командами, в которых пользователь состоит. - Проверка принадлежности к команде выполняется через
teams service.
⚠️ Ключевое отличие от incidents: большинство операций на запись требуют не просто членства в команде, а наличия роли owner в этой команде.
| Операция | Кто может выполнять |
|---|---|
| Создание расписания | admin или owner команды |
| Просмотр списка расписаний | admin (все) или участник (только свои команды) |
| Просмотр расписания | admin или участник команды |
| Обновление расписания | admin или owner команды |
| Удаление расписания | admin или owner команды |
| Просмотр смен | admin или участник команды |
| Добавление смены | admin или owner команды |
| Обновление смены | admin или owner команды |
| Удаление смены | admin или owner команды |
| Просмотр on-call по расписанию | admin или участник команды |
| Просмотр on-call по команде | admin или участник команды |
- Если у не-
adminнет ни одной команды,GET /api/v1/schedulesвозвращает пустой список. - Если указан
team_id, к которому пользователь не принадлежит, возвращается403. - Название (
name) расписания должно быть уникальным; при конфликте сервис возвращает409 Conflict. - При обновлении расписания
team_idизменить нельзя — команда задаётся только при создании.
schedules использует teams service для проверки командного доступа и роли пользователя.
Взаимодействие с teams service происходит по mTLS:
- клиент
schedulesзагружает клиентский сертификат и ключ; - сертификат
teams serviceвалидируется через указанный CA; - в TLS-конфигурации используется
ServerName: "teams"; - минимальная версия TLS — TLS 1.3;
- таймаут HTTP-клиента — 3 секунды.
Сертификат teams service должен содержать teams в CN или SAN.
Сервис использует следующие внутренние endpoints teams service:
GET /internal/users/:user_id/teams— получить список команд пользователя.GET /internal/teams/:team_id/members/:user_id— проверить членство пользователя в команде.GET /internal/teams/:team_id/members/:user_id/owner— проверить, является ли пользователь владельцем команды.
Если teams service недоступен, операции завершаются ошибкой 503 Service Unavailable.
Base URL: http://localhost:8004
Все ответы возвращаются в формате JSON:
// Успех
{ "success": true, "data": { ... } }
// Успех для delete-операций
{ "success": true, "message": "..." }
// Ошибка
{ "error": "описание ошибки" }Проверка работоспособности сервиса.
Авторизация: не требуется
Ответ 200:
{ "status": "ok" }GET /healthzпроверяет доступность HTTP-сервиса.GET /readyzдополнительно выполняет ping к базе данных.
Создание нового расписания.
Авторизация: Authorization: Bearer <access_token>
Права: глобальный admin или owner команды team_id
Тело запроса:
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
name |
string | ✅ | Название, от 2 до 255 символов; должно быть уникальным |
description |
string | ❌ | Описание расписания |
team_id |
uint | ✅ | ID команды; изменить после создания нельзя |
Пример запроса:
curl -X POST http://localhost:8004/api/v1/schedules \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{
"name": "Backend On-Call",
"description": "Primary on-call rotation for the backend team",
"team_id": 10
}'Ответ 201:
{
"success": true,
"data": {
"id": 1,
"name": "Backend On-Call",
"description": "Primary on-call rotation for the backend team",
"team_id": 10,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T10:00:00Z"
}
}Ошибки:
| Код | Причина |
|---|---|
400 |
Невалидное тело запроса |
401 |
Токен отсутствует или невалиден |
403 |
Нет прав owner в указанной команде |
409 |
Расписание с таким именем уже существует |
503 |
Не удалось проверить роль в teams service |
Возвращает список расписаний.
Авторизация: Authorization: Bearer <access_token>
Права:
adminвидит все расписания;- не-
adminвидит только расписания команд, в которых состоит.
Query параметры:
| Параметр | Тип | Описание |
|---|---|---|
team_id |
uint | Фильтр по команде |
Пример запроса:
curl "http://localhost:8004/api/v1/schedules?team_id=10" \
-H "Authorization: Bearer <access_token>"Ответ 200:
{
"success": true,
"data": [
{
"id": 1,
"name": "Backend On-Call",
"description": "Primary on-call rotation for the backend team",
"team_id": 10,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T10:00:00Z"
}
]
}Особенности:
- Если у пользователя нет доступных команд, возвращается пустой массив.
- Если указан
team_id, к которому пользователь не принадлежит, возвращается403.
Ошибки:
| Код | Причина |
|---|---|
401 |
Токен отсутствует или невалиден |
403 |
Нет доступа к указанной команде |
503 |
Не удалось получить членство пользователя из teams service |
500 |
Ошибка базы данных |
Возвращает расписание по ID.
Авторизация: Authorization: Bearer <access_token>
Права: admin или участник команды расписания
Пример запроса:
curl http://localhost:8004/api/v1/schedules/1 \
-H "Authorization: Bearer <access_token>"Ответ 200:
{
"success": true,
"data": {
"id": 1,
"name": "Backend On-Call",
"description": "Primary on-call rotation for the backend team",
"team_id": 10,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T10:00:00Z"
}
}Ошибки:
| Код | Причина |
|---|---|
400 |
Невалидный ID |
401 |
Токен отсутствует или невалиден |
403 |
Нет доступа к расписанию |
404 |
Расписание не найдено |
Обновление названия и описания расписания. Команда (team_id) не меняется.
Авторизация: Authorization: Bearer <access_token>
Права: admin или owner команды расписания
Тело запроса:
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
name |
string | ✅ | Новое название, от 2 до 255 символов |
description |
string | ❌ | Новое описание |
Пример запроса:
curl -X PUT http://localhost:8004/api/v1/schedules/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{
"name": "Backend On-Call v2",
"description": "Updated rotation schedule"
}'Ответ 200:
{
"success": true,
"data": {
"id": 1,
"name": "Backend On-Call v2",
"description": "Updated rotation schedule",
"team_id": 10,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T11:00:00Z"
}
}Ошибки:
| Код | Причина |
|---|---|
400 |
Невалидное тело запроса |
401 |
Токен отсутствует или невалиден |
403 |
Нет прав owner |
404 |
Расписание не найдено |
500 |
Ошибка обновления |
Удаление расписания.
Авторизация: Authorization: Bearer <access_token>
Права: admin или owner команды расписания
Пример запроса:
curl -X DELETE http://localhost:8004/api/v1/schedules/1 \
-H "Authorization: Bearer <access_token>"Ответ 200:
{
"success": true,
"message": "schedule deleted"
}Ошибки:
| Код | Причина |
|---|---|
401 |
Токен отсутствует или невалиден |
403 |
Нет прав owner |
404 |
Расписание не найдено |
500 |
Ошибка удаления |
Возвращает список смен расписания.
Авторизация: Authorization: Bearer <access_token>
Права: admin или участник команды расписания
Пример запроса:
curl http://localhost:8004/api/v1/schedules/1/shifts \
-H "Authorization: Bearer <access_token>"Ответ 200:
{
"success": true,
"data": [
{
"id": 1,
"schedule_id": 1,
"user_id": 42,
"starts_at": "2026-04-07T09:00:00Z",
"ends_at": "2026-04-08T09:00:00Z",
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T10:00:00Z"
}
]
}Ошибки:
| Код | Причина |
|---|---|
401 |
Токен отсутствует или невалиден |
403 |
Нет доступа к расписанию |
404 |
Расписание не найдено |
500 |
Ошибка чтения смен |
Добавление смены в расписание.
Авторизация: Authorization: Bearer <access_token>
Права: admin или owner команды расписания
Тело запроса:
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
user_id |
uint | ✅ | ID дежурного |
starts_at |
string (RFC3339) | ✅ | Начало смены |
ends_at |
string (RFC3339) | ✅ | Конец смены; должен быть позже starts_at |
Пример запроса:
curl -X POST http://localhost:8004/api/v1/schedules/1/shifts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{
"user_id": 42,
"starts_at": "2026-04-07T09:00:00Z",
"ends_at": "2026-04-08T09:00:00Z"
}'Ответ 201:
{
"success": true,
"data": {
"id": 1,
"schedule_id": 1,
"user_id": 42,
"starts_at": "2026-04-07T09:00:00Z",
"ends_at": "2026-04-08T09:00:00Z",
"created_at": "2026-04-01T10:05:00Z",
"updated_at": "2026-04-01T10:05:00Z"
}
}Ошибки:
| Код | Причина |
|---|---|
400 |
Невалидное тело запроса или ends_at не позже starts_at |
401 |
Токен отсутствует или невалиден |
403 |
Нет прав owner |
404 |
Расписание не найдено |
500 |
Ошибка добавления смены |
Обновление смены.
Авторизация: Authorization: Bearer <access_token>
Права: admin или owner команды расписания
Тело запроса:
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
user_id |
uint | ✅ | Новый дежурный |
starts_at |
string (RFC3339) | ✅ | Новое начало смены |
ends_at |
string (RFC3339) | ✅ | Новый конец смены; должен быть позже starts_at |
Пример запроса:
curl -X PUT http://localhost:8004/api/v1/schedules/1/shifts/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{
"user_id": 55,
"starts_at": "2026-04-07T09:00:00Z",
"ends_at": "2026-04-08T09:00:00Z"
}'Ответ 200:
{
"success": true,
"data": {
"id": 1,
"schedule_id": 1,
"user_id": 55,
"starts_at": "2026-04-07T09:00:00Z",
"ends_at": "2026-04-08T09:00:00Z",
"created_at": "2026-04-01T10:05:00Z",
"updated_at": "2026-04-01T12:00:00Z"
}
}Ошибки:
| Код | Причина |
|---|---|
400 |
Невалидное тело запроса или ends_at не позже starts_at |
401 |
Токен отсутствует или невалиден |
403 |
Нет прав owner |
404 |
Расписание или смена не найдены |
500 |
Ошибка обновления смены |
Удаление смены.
Авторизация: Authorization: Bearer <access_token>
Права: admin или owner команды расписания
Пример запроса:
curl -X DELETE http://localhost:8004/api/v1/schedules/1/shifts/1 \
-H "Authorization: Bearer <access_token>"Ответ 200:
{
"success": true,
"message": "shift deleted"
}Ошибки:
| Код | Причина |
|---|---|
401 |
Токен отсутствует или невалиден |
403 |
Нет прав owner |
404 |
Расписание или смена не найдены |
500 |
Ошибка удаления смены |
Возвращает текущего дежурного по конкретному расписанию (на момент запроса).
Авторизация: Authorization: Bearer <access_token>
Права: admin или участник команды расписания
Пример запроса:
curl http://localhost:8004/api/v1/schedules/1/oncall \
-H "Authorization: Bearer <access_token>"Ответ 200 (есть дежурный):
{
"success": true,
"data": {
"schedule_id": 1,
"schedule_name": "Backend On-Call",
"team_id": 10,
"user_id": 42,
"shift_id": 1,
"starts_at": "2026-04-07T09:00:00Z",
"ends_at": "2026-04-08T09:00:00Z"
}
}Ответ 200 (никто не дежурит):
{
"success": true,
"data": null,
"message": "no one is on-call right now"
}Ошибки:
| Код | Причина |
|---|---|
400 |
Невалидный ID расписания |
401 |
Токен отсутствует или невалиден |
403 |
Нет доступа к расписанию |
404 |
Расписание не найдено |
Возвращает всех текущих дежурных по всем расписаниям команды.
Авторизация: Authorization: Bearer <access_token>
Права: admin или участник команды
Query параметры:
| Параметр | Тип | Обязательное | Описание |
|---|---|---|---|
team_id |
uint | ✅ | ID команды |
Пример запроса:
curl "http://localhost:8004/api/v1/schedules/oncall?team_id=10" \
-H "Authorization: Bearer <access_token>"Ответ 200 (есть дежурные):
{
"success": true,
"data": [
{
"schedule_id": 1,
"schedule_name": "Backend On-Call",
"team_id": 10,
"user_id": 42,
"shift_id": 1,
"starts_at": "2026-04-07T09:00:00Z",
"ends_at": "2026-04-08T09:00:00Z"
},
{
"schedule_id": 2,
"schedule_name": "Infra On-Call",
"team_id": 10,
"user_id": 7,
"shift_id": 5,
"starts_at": "2026-04-07T00:00:00Z",
"ends_at": "2026-04-14T00:00:00Z"
}
]
}Ответ 200 (никто не дежурит):
{
"success": true,
"data": [],
"message": "no one is on-call right now"
}Ошибки:
| Код | Причина |
|---|---|
400 |
Отсутствует или невалидный team_id |
401 |
Токен отсутствует или невалиден |
403 |
Нет доступа к команде |
500 |
Ошибка поиска дежурных |