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).
Как это работает
На старте приложение:
- Загружает конфигурацию из
appsettings, переменных окружения и других стандартных источников .NET. - Проставляет значения по умолчанию для части переменных, если они не заданы.
- Создаёт рабочую директорию
data/, а внутри неё:jwt.txtдля сохранённого JWT;employees.jsonдля локального индекса сотрудников;keys/для ключей Data Protection;Plugins/для загружаемых плагинов.
- Настраивает логирование в консоль, а при наличии
TG_CHAT_IDиTG_TOKENещё и отправку ошибок в Telegram. - Подключает Sentry, Swagger, Prometheus, rate limiter, API key auth и OpenID Connect через
Microsoft.Identity.Web. - Загружает все плагины
*.plugin.dllизdata/Pluginsв отдельномAssemblyLoadContext. - Инициализирует Quartz-задачи:
- обновление JWT;
- обновление локального списка сотрудников;
- цепочку, при которой после успешного обновления JWT сразу запускается обновление сотрудников.
После запуска приложение выбирает источник JWT в таком порядке:
- Если задан
TOKEN, используется он, а фоновое обновление JWT отключается. - Иначе, если существует
data/jwt.txtи токен считается ещё пригодным, используется он. - Иначе приложение триггерит
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.htmlGET /api/docs— Swagger UIGET /api/docs/{documentName}/swagger.json— OpenAPI JSONGET /metrics— Prometheus metrics, только из локальных/приватных сетей
Schedule API
GET /api/schedule/getguid?fullname=...ТребуетX-Api-KeyGET /api/schedule/searchemployee?fullname=...GET /api/schedule/ics?attendeePersonId=...POST /api/schedule/ics
Proxy API
POST /api/proxy/events/searchPOST /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:5087https://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 и служебные ключи не меняются на каждый рестарт.
Плагины
Чтобы подключить плагин:
- Соберите библиотеку с именем вида
*.plugin.dll. - Убедитесь, что она реализует
ModeusSchedule.Abstractions.IPlugin. - Положите сборку и её зависимости в
data/Plugins/<имя-плагина>/. - Перезапустите приложение.
Хост:
- создаст для плагина отдельный
AssemblyLoadContext; - вызовет
ConfigureServices; - подключит MVC-контроллеры из сборки;
- вызовет
MapEndpointsпосле сборки приложения.
Что стоит учитывать
GET /api/schedule/searchemployeeзависит от успешной инициализации локального индекса сотрудников.- Если не настроен
AUTH_URLи не заданTOKEN, приложение не сможет полноценно работать сModeus. - Проверка срока жизни JWT в
jwt.txtоснована на времени сохранения файла, а не наexpclaim токена. - В коде включены
Sentrytracing и profiling с sample rate1.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Иванович"