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

367 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/` | код | Базовый 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Иванович"
```