Добавил README
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 1m16s

This commit is contained in:
2026-04-28 01:35:19 +03:00
parent 24eee21b20
commit d2cba1fb34

370
README.md
View File

@@ -1,8 +1,366 @@
# Прокси для расписания в Modeus # SfeduSchedule
## TODO `SfeduSchedule` это ASP.NET Core-приложение, которое работает как прокси и вспомогательный API поверх `Modeus`. Проект умеет:
- [x] Добавить RateLimiter - проксировать запросы к расписанию и поиску аудиторий в `Modeus`;
- [x] Добавить обработку ошибок при запросах к 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/` | код | Базовый URL`Modeus` |
| `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`:
```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-ключей чаще используют форму с двойным подчёркиванием:
```env
AzureAd__Instance=https://login.microsoftonline.com/
Sentry__Dsn=
```
## Локальный запуск
### Требования
- `.NET SDK 10`
- доступ к `Modeus`
- либо рабочий `TOKEN`, либо доступный `AUTH_URL`
### Запуск из исходников
```bash
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`.
Пример запуска:
```bash
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`.
Пример запуска:
```bash
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:
```bash
curl http://localhost:5087/
curl http://localhost:5087/api/docs
curl http://localhost:5087/metrics
```
Для защищённого эндпоинта:
```bash
curl -H "X-Api-Key: $API_KEY" \
"http://localhost:5087/api/schedule/getguid?fullname=Иванов%20Иван%20Иванович"
```