Files
SfeduSchedule/README.md
Sergey Karmanov d2cba1fb34
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 1m16s
Добавил README
2026-04-28 01:35:19 +03:00

24 KiB
Raw Blame History

SfeduSchedule

SfeduSchedule это ASP.NET Core-приложение, которое работает как прокси и вспомогательный API поверх Modeus. Проект умеет:

  • проксировать запросы к расписанию и поиску аудиторий в Modeus;
  • отдавать расписание в ICS;
  • искать GUID пользователя по ФИО;
  • поддерживать веб-вход через Microsoft для получения GUID текущего пользователя;
  • периодически обновлять JWT и локальный индекс сотрудников;
  • публиковать метрики Prometheus;
  • загружать внешние плагины из data/Plugins.

Основной проект находится в [SfeduSchedule/SfeduSchedule.csproj](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/SfeduSchedule.csproj), общие DTO и контракт плагинов в [ModeusSchedule.Abstractions](/home/serega404/Рабочий стол/SfeduSchedule/ModeusSchedule.Abstractions), пример плагина в [SfeduSchedule.Plugin.Sample](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule.Plugin.Sample).

Как это работает

На старте приложение:

  1. Загружает конфигурацию из appsettings, переменных окружения и других стандартных источников .NET.
  2. Проставляет значения по умолчанию для части переменных, если они не заданы.
  3. Создаёт рабочую директорию data/, а внутри неё:
    • jwt.txt для сохранённого JWT;
    • employees.json для локального индекса сотрудников;
    • keys/ для ключей Data Protection;
    • Plugins/ для загружаемых плагинов.
  4. Настраивает логирование в консоль, а при наличии TG_CHAT_ID и TG_TOKEN ещё и отправку ошибок в Telegram.
  5. Подключает Sentry, Swagger, Prometheus, rate limiter, API key auth и OpenID Connect через Microsoft.Identity.Web.
  6. Загружает все плагины *.plugin.dll из data/Plugins в отдельном AssemblyLoadContext.
  7. Инициализирует Quartz-задачи:
    • обновление JWT;
    • обновление локального списка сотрудников;
    • цепочку, при которой после успешного обновления JWT сразу запускается обновление сотрудников.

После запуска приложение выбирает источник JWT в таком порядке:

  1. Если задан TOKEN, используется он, а фоновое обновление JWT отключается.
  2. Иначе, если существует data/jwt.txt и токен считается ещё пригодным, используется он.
  3. Иначе приложение триггерит UpdateJwtJob, который запрашивает новый JWT у внешнего auth-сервиса.

Когда JWT уже есть, приложение использует его для запросов к Modeus.

Основные возможности

1. Проксирование Modeus API

Контроллер [ProxyController](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Controllers/ProxyController.cs) пробрасывает запросы в Modeus:

  • POST /api/proxy/events/search — получить расписание;
  • POST /api/proxy/rooms/search — искать аудитории.

Если Modeus отвечает ошибкой, сервис возвращает соответствующий HTTP-статус и текст с префиксом Proxied Modeus:.

2. Работа с расписанием

Контроллер [ScheduleController](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Controllers/ScheduleController.cs) предоставляет:

  • GET /api/schedule/getguid?fullname=... — получить GUID по полному имени, защищено X-Api-Key;
  • GET /api/schedule/searchemployee?fullname=... — искать сотрудников по локальному индексу;
  • GET /api/schedule/ics?attendeePersonId=... — получить ICS для одного пользователя;
  • POST /api/schedule/ics — получить ICS по кастомному ModeusScheduleRequest.

Генерация ICS происходит в [ModeusService](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Services/ModeusService.cs): сервис получает JSON расписания из Modeus, вытаскивает аудитории, преподавателей, краткие названия дисциплин и формирует календарь через Ical.Net.

Диапазон у GET /api/schedule/ics зашит в коде: от текущего момента минус 7 дней до плюс 1 месяца.

3. GUID через Microsoft-вход

Контроллер [SfeduController](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Controllers/SfeduController.cs) отдаёт:

  • GET /api/sfedu/guid

Маршрут защищён схемой OpenIdConnect. После логина приложение берёт claim name, ищет GUID через Modeus и:

  • либо возвращает GUID текстом;
  • либо делает redirect на redirectUri?guid=..., если передан параметр redirectUri.

Для этого сценария должны быть корректно настроены переменные AzureAd:*.

4. Локальный индекс сотрудников

[ModeusEmployeeService](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Services/ModeusEmployeeService.cs) хранит в памяти словарь сотрудников и их должностей, а также сохраняет его в data/employees.json.

Обновление выполняет Quartz job [UpdateEmployeesJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateEmployeesJob.cs):

  • ходит в Modeus;
  • загружает большой список сотрудников;
  • группирует записи по ФИО;
  • сохраняет результат на диск;
  • повторяет попытки при временных сбоях.

Если сервис ещё не инициализирован, GET /api/schedule/searchemployee возвращает 503.

5. JWT lifecycle

Quartz job [UpdateJwtJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateJWTJob.cs):

  • идёт во внешний auth-сервис по AUTH_URL;
  • опционально передаёт X-API-Key, если задан AUTH_API_KEY;
  • ожидает JSON вида { "jwt": "..." };
  • записывает JWT в runtime-конфигурацию, в ModeusHttpClient и в data/jwt.txt.

Если TOKEN задан вручную, job не получает триггер по cron и приложение использует этот токен как основной.

6. Метрики и ограничения доступа

Приложение публикует метрики prometheus-net на /metrics.

Для /metrics добавлен middleware [LocalNetworksOnlyMiddleware](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs):

  • разрешён только GET;
  • доступ открыт только из loopback, RFC1918 private networks, link-local и local IPv6 диапазонов;
  • для остальных адресов возвращается 403.

7. Ограничение частоты запросов

Для части API включён rate limit с фиксированным окном:

  • по умолчанию 40 запросов;
  • за окно 10 секунд;
  • ключом клиента считается первый IP из X-Forwarded-For, либо RemoteIpAddress.

При превышении лимита приложение отвечает 429 и добавляет заголовок Retry-After.

GET /api/schedule/getguid специально исключён из rate limiting.

8. Плагины

На старте хост ищет все *.plugin.dll внутри data/Plugins и загружает найденные сборки как плагины. Плагин должен реализовывать интерфейс [IPlugin](/home/serega404/Рабочий стол/SfeduSchedule/ModeusSchedule.Abstractions/IPlugin.cs), чтобы:

  • зарегистрировать свои сервисы в DI через ConfigureServices;
  • добавить minimal API маршруты через MapEndpoints.

MVC-контроллеры из плагинов тоже подключаются автоматически через ApplicationPart.

В репозитории есть пример в [SfeduSchedule.Plugin.Sample/Plugin.cs](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule.Plugin.Sample/Plugin.cs).

API endpoints

Публичные и служебные

  • GET / — статическая стартовая страница из wwwroot/index.html
  • GET /api/docs — Swagger UI
  • GET /api/docs/{documentName}/swagger.json — OpenAPI JSON
  • GET /metrics — Prometheus metrics, только из локальных/приватных сетей

Schedule API

  • GET /api/schedule/getguid?fullname=... Требует X-Api-Key
  • GET /api/schedule/searchemployee?fullname=...
  • GET /api/schedule/ics?attendeePersonId=...
  • POST /api/schedule/ics

Proxy API

  • POST /api/proxy/events/search
  • POST /api/proxy/rooms/search

Sfedu API

  • GET /api/sfedu/guid Требует вход через Microsoft / OpenID Connect

Переменные окружения

Ниже перечислены переменные, которые реально используются кодом или docker-compose файлами проекта.

Ключевые переменные приложения

Переменная Обязательность Значение по умолчанию Где используется Назначение
MODEUS_URL Нет https://sfedu.modeus.org/ код Базовый URLModeus
TOKEN Условно обязательна нет код Готовый JWT дляModeus; если не задан, приложение попытается получить токен через AUTH_URL
AUTH_URL Условно обязательна http://msauth:8080/auth/ms код URL сервиса, который возвращает новый JWT
AUTH_API_KEY Нет пусто код API key для вызоваAUTH_URL, передаётся как X-API-Key
API_KEY Условно обязательна нет код Ключ для маршрутов, защищённых схемойApiKey
UPDATE_JWT_CRON Нет 0 0 4 ? * * код Cron для фонового обновления JWT
UPDATE_EMPLOYEES_CRON Нет 0 0 6 ? * * код Cron для фонового обновления индекса сотрудников
PERMIT_LIMIT Нет 40 код Лимит запросов в окне rate limiter
TIME_LIMIT Нет 10 код Длина окна rate limiter в секундах
TZ Нет Europe/Moscow код Таймзона для запросов кModeus и генерации ICS
TG_CHAT_ID Нет нет код Chat ID для Telegram-логирования
TG_TOKEN Нет нет код Bot token для Telegram-логирования
Sentry:Dsn Нет пустая строка код DSN для Sentry

Переменные для Microsoft / Azure AD

Эти значения напрямую читает Microsoft.Identity.Web. В compose-файлах они передаются в контейнер, а в коде используются для OpenIdConnect-схемы.

Переменная Обязательность Значение по умолчанию Назначение
AzureAd:Instance Условно обязательна нет Базовый URL identity provider
AzureAd:TenantId Условно обязательна нет Tenant ID или tenant domain
AzureAd:ClientId Условно обязательна нет Client ID приложения
AzureAd:ClientSecret Условно обязательна нет Client secret приложения
AzureAd:Domain Условно обязательна нет Azure AD domain
AzureAd:CallbackPath Условно обязательна нет Callback path для OIDC

Обязательность у AzureAd:* условная: без них приложение может быть полезно как API-прокси, но маршрут GET /api/sfedu/guid и связанный login-flow работать корректно не будут.

Переменные из docker-compose

Переменная Обязательность Значение по умолчанию Назначение
ASPNETCORE_FORWARDEDHEADERS_ENABLED Нет нет В compose выставляется вtrue, чтобы приложение корректнее работало за reverse proxy

Что значит "условно обязательна"

Условно обязательна означает, что переменная нужна не всегда, а только для конкретного сценария:

  • нужен TOKEN или рабочий AUTH_URL, чтобы ходить в Modeus;
  • нужен API_KEY, если вы хотите использовать GET /api/schedule/getguid;
  • нужны AzureAd:*, если используется GET /api/sfedu/guid;
  • нужны TG_CHAT_ID и TG_TOKEN, если хотите логирование в Telegram;
  • нужен Sentry:Dsn, если хотите отправлять ошибки и трейсы в Sentry.

Пример .env

Для запуска через docker compose можно использовать примерно такой .env:

API_KEY=change-me

AUTH_URL=http://msauth:8080/auth/ms
AUTH_API_KEY=

MODEUS_URL=https://sfedu.modeus.org/
TZ=Europe/Moscow

UPDATE_JWT_CRON=0 0 4 ? * *
UPDATE_EMPLOYEES_CRON=0 0 6 ? * *

PERMIT_LIMIT=40
TIME_LIMIT=10

TG_CHAT_ID=
TG_TOKEN=

SENTRY_DSN=

AzureAd:Instance=https://login.microsoftonline.com/
AzureAd:TenantId=sfedu.ru
AzureAd:ClientId=
AzureAd:ClientSecret=
AzureAd:Domain=sfedu.onmicrosoft.com
AzureAd:CallbackPath=/signin-oidc

Если вы передаёте переменные именно как environment variables вне docker compose, для иерархических .NET-ключей чаще используют форму с двойным подчёркиванием:

AzureAd__Instance=https://login.microsoftonline.com/
Sentry__Dsn=

Локальный запуск

Требования

  • .NET SDK 10
  • доступ к Modeus
  • либо рабочий TOKEN, либо доступный AUTH_URL

Запуск из исходников

dotnet restore SfeduSchedule.sln
dotnet run --project SfeduSchedule/SfeduSchedule.csproj

Профили локального запуска прописаны в [launchSettings.json](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Properties/launchSettings.json):

  • http://localhost:5087
  • https://localhost:7146

Запуск в Docker

Production compose

Файл [docker-compose-prod.yml](/home/serega404/Рабочий стол/SfeduSchedule/docker-compose-prod.yml):

  • поднимает контейнер SfeduSchedule;
  • публикует порт 8088 -> 8080;
  • использует именованный volume data:/app/data;
  • ожидает готовый образ git.zetcraft.ru/serega404/sfeduschedule:main.

Пример запуска:

docker compose -f docker-compose-prod.yml up -d

Test compose

Файл [docker-compose-test.yml](/home/serega404/Рабочий стол/SfeduSchedule/docker-compose-test.yml) предназначен для локальной сборки и монтирует ./data:/app/data.

Пример запуска:

docker compose -f docker-compose-test.yml up --build

Важно: в текущем виде docker-compose-test.yml ссылается на dockerfile: Dockerfile при context: ./SfeduSchedule, тогда как Dockerfile лежит в корне репозитория. Перед использованием этот compose-файл, вероятно, нужно скорректировать под фактическую структуру проекта.

Структура данных в data/

Во время работы приложение заполняет:

  • data/jwt.txt — первая строка JWT, вторая строка timestamp его сохранения;
  • data/employees.json — локальный индекс сотрудников;
  • data/keys/ — ключи Data Protection;
  • data/Plugins/ — каталог для плагинов.

Эту директорию стоит сохранять между рестартами контейнера.

Безопасность и эксплуатационные детали

  • API_KEY можно передавать в заголовке X-Api-Key, а также в query string как api_key.
  • Для X-Api-Key используется сравнение в постоянное время.
  • Приложение обрабатывает X-Forwarded-For, X-Forwarded-Proto и X-Forwarded-Host.
  • В ответ на каждый HTTP-запрос добавляется X-Correlation-ID.
  • Swagger опубликован по адресу /api/docs, а не по стандартному /swagger.
  • Ключи Data Protection сохраняются на диск, поэтому cookie и служебные ключи не меняются на каждый рестарт.

Плагины

Чтобы подключить плагин:

  1. Соберите библиотеку с именем вида *.plugin.dll.
  2. Убедитесь, что она реализует ModeusSchedule.Abstractions.IPlugin.
  3. Положите сборку и её зависимости в data/Plugins/<имя-плагина>/.
  4. Перезапустите приложение.

Хост:

  • создаст для плагина отдельный AssemblyLoadContext;
  • вызовет ConfigureServices;
  • подключит MVC-контроллеры из сборки;
  • вызовет MapEndpoints после сборки приложения.

Что стоит учитывать

  • GET /api/schedule/searchemployee зависит от успешной инициализации локального индекса сотрудников.
  • Если не настроен AUTH_URL и не задан TOKEN, приложение не сможет полноценно работать с Modeus.
  • Проверка срока жизни JWT в jwt.txt основана на времени сохранения файла, а не на exp claim токена.
  • В коде включены Sentry tracing и profiling с sample rate 1.0, это может быть важно для нагрузки и объёма телеметрии.

Проверка после запуска

Минимальный smoke check:

curl http://localhost:5087/
curl http://localhost:5087/api/docs
curl http://localhost:5087/metrics

Для защищённого эндпоинта:

curl -H "X-Api-Key: $API_KEY" \
  "http://localhost:5087/api/schedule/getguid?fullname=Иванов%20Иван%20Иванович"