Skip to content

Latest commit

 

History

History
721 lines (544 loc) · 21.9 KB

File metadata and controls

721 lines (544 loc) · 21.9 KB

schedules service

English version

Сервис управления расписаниями дежурств системы Induty.
Отвечает за создание расписаний, управление сменами (shifts) и определение текущего дежурного (on-call).


Содержание


Конфигурация

Сервис читает конфиг из config.yaml. Любое поле может быть переопределено переменной окружения.

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 или участник команды

Особенности team-scoped доступа

  • Если у не-admin нет ни одной команды, GET /api/v1/schedules возвращает пустой список.
  • Если указан team_id, к которому пользователь не принадлежит, возвращается 403.
  • Название (name) расписания должно быть уникальным; при конфликте сервис возвращает 409 Conflict.
  • При обновлении расписания team_id изменить нельзя — команда задаётся только при создании.

Интеграция с teams service

schedules использует teams service для проверки командного доступа и роли пользователя.

mTLS

Взаимодействие с teams service происходит по mTLS:

  • клиент schedules загружает клиентский сертификат и ключ;
  • сертификат teams service валидируется через указанный CA;
  • в TLS-конфигурации используется ServerName: "teams";
  • минимальная версия TLS — TLS 1.3;
  • таймаут HTTP-клиента — 3 секунды.

Сертификат teams service должен содержать teams в CN или SAN.

Внутренние запросы к teams

Сервис использует следующие внутренние 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.


API

Base URL: http://localhost:8004

Все ответы возвращаются в формате JSON:

// Успех
{ "success": true, "data": { ... } }

// Успех для delete-операций
{ "success": true, "message": "..." }

// Ошибка
{ "error": "описание ошибки" }

GET /healthz /readyz

Проверка работоспособности сервиса.

Авторизация: не требуется

Ответ 200:

{ "status": "ok" }
  • GET /healthz проверяет доступность HTTP-сервиса.
  • GET /readyz дополнительно выполняет ping к базе данных.

POST /api/v1/schedules

Создание нового расписания.

Авторизация: 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

GET /api/v1/schedules

Возвращает список расписаний.

Авторизация: 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 Ошибка базы данных

GET /api/v1/schedules/:id

Возвращает расписание по 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 Расписание не найдено

PUT /api/v1/schedules/:id

Обновление названия и описания расписания. Команда (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 Ошибка обновления

DELETE /api/v1/schedules/:id

Удаление расписания.

Авторизация: 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 Ошибка удаления

GET /api/v1/schedules/:id/shifts

Возвращает список смен расписания.

Авторизация: 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 Ошибка чтения смен

POST /api/v1/schedules/:id/shifts

Добавление смены в расписание.

Авторизация: 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 Ошибка добавления смены

PUT /api/v1/schedules/:id/shifts/:shift_id

Обновление смены.

Авторизация: 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 Ошибка обновления смены

DELETE /api/v1/schedules/:id/shifts/:shift_id

Удаление смены.

Авторизация: 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 Ошибка удаления смены

GET /api/v1/schedules/:id/oncall

Возвращает текущего дежурного по конкретному расписанию (на момент запроса).

Авторизация: 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 Расписание не найдено

GET /api/v1/schedules/oncall

Возвращает всех текущих дежурных по всем расписаниям команды.

Авторизация: 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 Ошибка поиска дежурных